Filter by date then sort by tag

It may be that the Result panel wasn’t open in Script Editor.

(Bent arrow icon, bottom margin of the window)

I’ll aim to sketch something like that over the weekend, if no one else has drafted it first.

Thank you!!!

Here’s an experimental draft (use with dummy data only) which aims to:

  • sort the top level by @ps values
  • sort each second level range of peers by @as values

and create a new document in that sorted order.

( Scripted mutation of data I personally avoid – particularly when sharing scripts with others – scripting the creation of a transformed copy is one thing, but so much more can go wrong in an attempted in-place mutation )

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rough draft - not for use in production.

    // New copy of front document sorted at two levels:
    // Top level by ps values
    // Second level by as values.

    // Ver 0.01
    // Rob Trew 2021
    // (MIT license and caveats)

    // ------------------- JXA CONTEXT -------------------
    // main :: IO () -> Either Message Document
    const main = () => {
        const
            taskpaper = Application("TaskPaper"),
            windows = taskpaper.windows;

        return 0 < windows.length ? (() => {
            const
                outlineText = windows.at(0).document
                .evaluate({
                    script: `${TaskPaperContext}`
                }),
                sortedDoc = new taskpaper.Document({
                    textContents: outlineText
                });

            return (
                taskpaper.documents.push(
                    sortedDoc
                ),
                sortedDoc.textContents()
            );
        })() : "No document windows open in TaskPaper";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const psOrder = {
            urgent: 0,
            perpetual: 1,
            current: 2,
            following: 3,
            future: 4,
            someday: 5
        };

        const asOrder = {
            now: 0,
            next: 1,
            later: 2,
            waiting: 3,
            someday: 4
        };

        // tpMain :: () -> IO String
        const tpMain = () =>
            tabOutlineFromForest(
                fmapTree(
                    x => x.bodyString
                )(
                    distinctlySortedLevels([
                        x => psOrder[
                            x.root.getAttribute("data-ps")
                        ],
                        x => asOrder[
                            x.root.getAttribute("data-as")
                        ]
                    ])(
                        pureTreeTP3(
                            editor.outline.root
                        )
                    )
                )
                .nest
            );

        // ------- DISTINCT SORTS OF UPPER LEVELS --------

        // distinctlySortedLevels ::
        // [Node -> a] -> Tree TPNode -> Tree TPNode
        const distinctlySortedLevels = accessors => {
            const go = fs => t =>
                Node(t.root)(
                    0 < fs.length ? (
                        sortOn(fs[0])(t.nest)
                        .map(go(fs.slice(1)))
                    ) : t.nest
                );

            return go(accessors);
        };

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


        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


        // 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 = t => Node(
                f(t.root)
            )(
                t.nest.map(go)
            );

            return go;
        };


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };


        // sortOn :: Ord b => (a -> b) -> [a] -> [a]
        const sortOn = f =>
            // Equivalent to sortBy(comparing(f)), but with f(x)
            // evaluated only once for each x in xs.
            // ('Schwartzian' decorate-sort-undecorate).
            xs => xs.map(
                x => [f(x), x]
            )
            .sort(comparing(x => x[0]))
            .map(x => x[1]);


        // tabOutlineFromForest :: Forest String -> String
        const tabOutlineFromForest = trees => {
            const go = tabs => tree => [
                `${tabs}${tree.root}`,
                ...tree.nest.flatMap(go(`\t${tabs}`))
            ];

            return trees.flatMap(go("")).join("\n");
        };


        // TASKPAPER CONTEXT MAIN ---
        return tpMain();
    };

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

FWIW if we got the sorting to the point where it seems to be well tested and doing what you need, then technically you could replace the active document text contents with the newly sorted version by amending the return of the JXA main avoid to something like:

Expand disclosure triangle to view JS Source
return 0 < windows.length ? (() => {
    const
        doc = windows.at(0).document,
        outlineText = doc.evaluate({
            script: `${TaskPaperContext}`
        });

    return (
        doc.textContents = outlineText,
        doc.textContents()
    );
})() : "No document windows open in TaskPaper";

but all caveats about the risks of scripted document mutation still apply …

In fact, I think Jesse’s .reloadSerialization(serialization, options?) would be safer – especially if it be performed within a .groupUndoAndChanges context, to enable ⌘Z – though that may be a long shot …

At the moment however, there seems to be some missing text in the documentation of .reloadSerialization(serialization, options?) at:

Outline Β· GitBook

(Some fields missing or illegible in the options section for reloadSerialization ?)

So I don’t feel completely confident about testing it yet : -)

And as I say – mutation is not my thing – I prefer to make and use modified copies.

An experimental :skull_and_crossbones:mutating :skull_and_crossbones: draft (aiming for ⌘Z reversibility) – test with caution:

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rough draft - not for use in production,
    // or with data that is not backed up.

    // Document experimentally updated in place
    // with two level sort:
    // - by @ps tag value at top level
    // - by @as tag value at second level.
    // (
    //     See psOrder and asOrder below for ordinal
    //     interpretations of tag values.
    // )

    // Ver 0.04
    // Rob Trew 2021
    // (MIT license and caveats)

    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY
    // OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
    // LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
    // BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
    // ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    // ------------------- JXA CONTEXT -------------------
    // main :: IO () -> Either Message Document
    const main = () => {
        const
            taskpaper = Application("TaskPaper"),
            windows = taskpaper.windows;

        return 0 < windows.length ? (() =>
            windows.at(0).document.evaluate({
                script: `${TaskPaperContext}`
            })
        )() : "No document windows open in TaskPaper";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const psOrder = {
            urgent: 0,
            perpetual: 1,
            current: 2,
            following: 3,
            future: 4,
            someday: 5
        };

        const asOrder = {
            now: 0,
            next: 1,
            later: 2,
            waiting: 3,
            someday: 4
        };

        // tpMain :: () -> IO String
        const tpMain = () => {
            const
                outline = editor.outline,
                sortedTree = distinctlySortedLevels([
                    x => psOrder[
                        x.root.getAttribute("data-ps")
                    ],
                    x => asOrder[
                        x.root.getAttribute("data-as")
                    ]
                ])(
                    pureTreeTP3(outline.root)
                );

            // --------------- MUTATION ------------------
            outline.groupUndoAndChanges(
                () => foldTree(
                    updatedPeerOrder(outline)
                )(
                    sortedTree
                )
            );
        };

        // ------- DISTINCT SORTS OF UPPER LEVELS --------

        // distinctlySortedLevels ::
        // [Node -> a] -> Tree TPNode -> Tree TPNode
        const distinctlySortedLevels = accessors => {
            const go = fs => tree =>
                Node(tree.root)(
                    0 < fs.length ? (
                        sortOn(fs[0])(tree.nest)
                        .map(go(fs.slice(1)))
                    ) : tree.nest
                );

            return go(accessors);
        };


        // updatedPeerOrder :: TPOutline ->
        // TPNode -> [TPNode] -> IO Tree TPNode
        const updatedPeerOrder = outline =>
            // updatedPeerOrder(outline)
            // can be used as an argument to foldTree
            // for bottom up mapping of a new multi-level
            // sorted Tree Node to the TaskPaper model.
            tpNode => children => Node(tpNode)((
                1 < children.length && (
                    children.reduceRight(
                        (a, tree) => {
                            const x = tree.root;

                            return (
                                a.id !== x.id && (
                                    outline
                                    .insertItemsBefore(x, a)
                                ),
                                x
                            );
                        },
                        tpNode.firstChild
                    )
                ),
                tpNode.children
            ));


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

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


        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


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


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };


        // sortOn :: Ord b => (a -> b) -> [a] -> [a]
        const sortOn = f =>
            // Equivalent to sortBy(comparing(f)), but with f(x)
            // evaluated only once for each x in xs.
            // ('Schwartzian' decorate-sort-undecorate).
            xs => xs.map(
                x => [f(x), x]
            )
            .sort(comparing(x => x[0]))
            .map(x => x[1]);


        // TASKPAPER CONTEXT MAIN ---
        return tpMain();
    };

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

Hey @complexpoint - Thanks so much for the scripts, I’ve gave it a test run with the sample file and it worked perfectly. I also tested it with a slightly modified file (see below), in which I added blank lines between projects, which does get some unexpected results.

New test file
Inbox:
- 🟀 Some inbox item | @as(waiting)
- πŸ”΄ Something else | @as(now)
- A status-less task


Buy a boat: @ps(someday)
	- πŸ”΅ Subscribe to a boating magazine | @as(someday)
	- 🟠 Do my skippers license | @as(next)
	- 🟑 Go down to the boat shop and ask for advice | @as(later)
	- πŸ”΄ Do reasearch on boats | @as(now)

Go to the grocery store: @ps(perpetual)
	- 🟠 But apples | @as(next)
		notes about buying apples
	- πŸ”΄ Find out when they have salmon in stock | @as(now)

Paint the house: @ps(following)
	- 🟀  Waiting to hear back from Steve to help out | @as(waiting)
	- 🟠 Start sanding the house | @as(next)
	- πŸ”΄ Go to the shop and buy paint | @as(now)

Buy birthday gift for James: @ps(urgent)
	- 🟠 Go to the mall | @as(next)
	- 🟑 Wrap the gift | @as(later)
	- πŸ”΄ Speak to Marie about what James likes | @as(now)
	- 🟑 Write a card | @as(later)
		some more notes aboutu writing a card
	- 🟀 Give gift to James | @as(waiting)

Clean the pool: @ps(current)
	- 🟠 Put chlorine into the pool | @as(next)
	- 🟑 Run pool pump | @as(later)
	- πŸ”΄ Buy chlorine | @as(now)
	- πŸ”΅ Swim | @as(someday)

Go bowling: @ps(future)
	- πŸ”΄ Learn how to bowl | @as(now)
	- πŸ”΅ Buy bowling shoes  | @as(someday)
	- πŸ”΄ Find a local club | @as(now)

To provide feedback in the form you posted before:

Current Result: With spaces between projects the children tasks all sort as originally intended, however not all projects get sorted correctly. Here is the sorted result that I get when running the script on the sample file above.

Current result
Inbox:
- 🟀 Some inbox item | @as(waiting)
- πŸ”΄ Something else | @as(now)
- A status-less task


Buy birthday gift for James: @ps(urgent)
	- πŸ”΄ Speak to Marie about what James likes | @as(now)
	- 🟠 Go to the mall | @as(next)
	- 🟑 Wrap the gift | @as(later)
	- 🟑 Write a card | @as(later)
		some more notes aboutu writing a card
	- 🟀 Give gift to James | @as(waiting)
Go to the grocery store: @ps(perpetual)
	- πŸ”΄ Find out when they have salmon in stock | @as(now)
	- 🟠 But apples | @as(next)
		notes about buying apples
Go bowling: @ps(future)
	- πŸ”΄ Learn how to bowl | @as(now)
	- πŸ”΄ Find a local club | @as(now)
	- πŸ”΅ Buy bowling shoes  | @as(someday)
Buy a boat: @ps(someday)
	- πŸ”΄ Do reasearch on boats | @as(now)
	- 🟠 Do my skippers license | @as(next)
	- 🟑 Go down to the boat shop and ask for advice | @as(later)
	- πŸ”΅ Subscribe to a boating magazine | @as(someday)


Paint the house: @ps(following)
	- πŸ”΄ Go to the shop and buy paint | @as(now)
	- 🟠 Start sanding the house | @as(next)
	- 🟀  Waiting to hear back from Steve to help out | @as(waiting)


Clean the pool: @ps(current)
	- πŸ”΄ Buy chlorine | @as(now)
	- 🟠 Put chlorine into the pool | @as(next)
	- 🟑 Run pool pump | @as(later)
	- πŸ”΅ Swim | @as(someday)

Expected result: All projects are sorted - ideally the spaces (and notes) between projects are maintained, but I can see how this would be tricky. If all projects are sorted and the blank spaces are moved to the end of the file, that would also work.

Ps. apologies for not getting back to you sooner - been swamped this side. Thanks again for the amazing help!

It looks as if your expectation may have been that those three items are children of the Inbox project.

The TaskPaper format is a strictly tab-indented outline, so in your sample there, lines 2 to 4 are not in fact children of Inbox – they are peers of it – top level items like Inbox itself, which, in that sample, has no children/contents.

(You just need to tab-indent them – making them contents of the Inbox project. Folding and expanding the Inbox: project in the GUI – Outline > Collapse Items and Outline > Expand Items – will enable you to check)

1 Like

Ah yes you’re right, but I still seem to get some strange results when there are some blank lines between projects.

Again, you would need to show:

  1. source
  2. output
  3. expected output
1. Script (unedited)
(() => {
    "use strict";

    // Rough draft - not for use in production.

    // New copy of front document sorted at two levels:
    // Top level by ps values
    // Second level by as values.

    // Ver 0.01
    // Rob Trew 2021
    // (MIT license and caveats)

    // ------------------- JXA CONTEXT -------------------
    // main :: IO () -> Either Message Document
    const main = () => {
        const
            taskpaper = Application("TaskPaper"),
            windows = taskpaper.windows;

        return 0 < windows.length ? (() => {
            const
                outlineText = windows.at(0).document
                .evaluate({
                    script: `${TaskPaperContext}`
                }),
                sortedDoc = new taskpaper.Document({
                    textContents: outlineText
                });

            return (
                taskpaper.documents.push(
                    sortedDoc
                ),
                sortedDoc.textContents()
            );
        })() : "No document windows open in TaskPaper";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const psOrder = {
            urgent: 0,
            perpetual: 1,
            current: 2,
            following: 3,
            future: 4,
            someday: 5
        };

        const asOrder = {
            now: 0,
            next: 1,
            later: 2,
            waiting: 3,
            someday: 4
        };

        // tpMain :: () -> IO String
        const tpMain = () =>
            tabOutlineFromForest(
                fmapTree(
                    x => x.bodyString
                )(
                    distinctlySortedLevels([
                        x => psOrder[
                            x.root.getAttribute("data-ps")
                        ],
                        x => asOrder[
                            x.root.getAttribute("data-as")
                        ]
                    ])(
                        pureTreeTP3(
                            editor.outline.root
                        )
                    )
                )
                .nest
            );

        // ------- DISTINCT SORTS OF UPPER LEVELS --------

        // distinctlySortedLevels ::
        // [Node -> a] -> Tree TPNode -> Tree TPNode
        const distinctlySortedLevels = accessors => {
            const go = fs => t =>
                Node(t.root)(
                    0 < fs.length ? (
                        sortOn(fs[0])(t.nest)
                        .map(go(fs.slice(1)))
                    ) : t.nest
                );

            return go(accessors);
        };

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


        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


        // 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 = t => Node(
                f(t.root)
            )(
                t.nest.map(go)
            );

            return go;
        };


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };


        // sortOn :: Ord b => (a -> b) -> [a] -> [a]
        const sortOn = f =>
            // Equivalent to sortBy(comparing(f)), but with f(x)
            // evaluated only once for each x in xs.
            // ('Schwartzian' decorate-sort-undecorate).
            xs => xs.map(
                x => [f(x), x]
            )
            .sort(comparing(x => x[0]))
            .map(x => x[1]);


        // tabOutlineFromForest :: Forest String -> String
        const tabOutlineFromForest = trees => {
            const go = tabs => tree => [
                `${tabs}${tree.root}`,
                ...tree.nest.flatMap(go(`\t${tabs}`))
            ];

            return trees.flatMap(go("")).join("\n");
        };


        // TASKPAPER CONTEXT MAIN ---
        return tpMain();
    };

    // JXA CONTEXT MAIN ---
    return main();
})();
2. Test File
Inbox:
	- 🟀 Some inbox item | @as(waiting)
	- πŸ”΄ Something else | @as(now)
	- A status-less task


Buy a boat: @ps(someday)
	- πŸ”΅ Subscribe to a boating magazine | @as(someday)
	- 🟠 Do my skippers license | @as(next)
	- 🟑 Go down to the boat shop and ask for advice | @as(later)
	- πŸ”΄ Do reasearch on boats | @as(now)
Go to the grocery store: @ps(perpetual)
	- 🟠 But apples | @as(next)
		notes about buying apples
	- πŸ”΄ Find out when they have salmon in stock | @as(now)

Paint the house: @ps(following)
	- 🟀  Waiting to hear back from Steve to help out | @as(waiting)
	- 🟠 Start sanding the house | @as(next)
	- πŸ”΄ Go to the shop and buy paint | @as(now)

Buy birthday gift for James: @ps(urgent)
	- 🟠 Go to the mall | @as(next)
	- 🟑 Wrap the gift | @as(later)
	- πŸ”΄ Speak to Marie about what James likes | @as(now)
	- 🟑 Write a card | @as(later)
		some more notes aboutu writing a card
	- 🟀 Give gift to James | @as(waiting)

Clean the pool: @ps(current)
	- 🟠 Put chlorine into the pool | @as(next)
	- 🟑 Run pool pump | @as(later)
	- πŸ”΄ Buy chlorine | @as(now)
	- πŸ”΅ Swim | @as(someday)

Go bowling: @ps(future)
	- πŸ”΄ Learn how to bowl | @as(now)
	- πŸ”΅ Buy bowling shoes  | @as(someday)
	- πŸ”΄ Find a local club | @as(now)
3. Result
Inbox:
	- πŸ”΄ Something else | @as(now)
	- 🟀 Some inbox item | @as(waiting)
	- A status-less task


Buy birthday gift for James: @ps(urgent)
	- πŸ”΄ Speak to Marie about what James likes | @as(now)
	- 🟠 Go to the mall | @as(next)
	- 🟑 Wrap the gift | @as(later)
	- 🟑 Write a card | @as(later)
		some more notes aboutu writing a card
	- 🟀 Give gift to James | @as(waiting)
Go to the grocery store: @ps(perpetual)
	- πŸ”΄ Find out when they have salmon in stock | @as(now)
	- 🟠 But apples | @as(next)
		notes about buying apples
Buy a boat: @ps(someday)
	- πŸ”΄ Do reasearch on boats | @as(now)
	- 🟠 Do my skippers license | @as(next)
	- 🟑 Go down to the boat shop and ask for advice | @as(later)
	- πŸ”΅ Subscribe to a boating magazine | @as(someday)

Paint the house: @ps(following)
	- πŸ”΄ Go to the shop and buy paint | @as(now)
	- 🟠 Start sanding the house | @as(next)
	- 🟀  Waiting to hear back from Steve to help out | @as(waiting)


Clean the pool: @ps(current)
	- πŸ”΄ Buy chlorine | @as(now)
	- 🟠 Put chlorine into the pool | @as(next)
	- 🟑 Run pool pump | @as(later)
	- πŸ”΅ Swim | @as(someday)

Go bowling: @ps(future)
	- πŸ”΄ Learn how to bowl | @as(now)
	- πŸ”΄ Find a local club | @as(now)
	- πŸ”΅ Buy bowling shoes  | @as(someday)

I’ve noticed the following:

  1. If there is a single line between every project (the last task of one project and the next project) then nothing gets sorted if the script is run
  2. If there is a line between every project, except 2 projects (as is the case in the sample file) then there seems to be a strange sorting result
  3. In all cases the children tasks are sorted correctly.

As for the desired behavior, I’d expect:

  • If there are spaces between projects no sorting of projects occurs
  • For each set of projects without spaces between them, they are sorted

Again, show works where tell fails.

You would need to show me what that means to you.

Incidentally, remember that not all β€œblank-looking” lines are the same.

A peer blank line (sharing a parent with lines before and aft) has the same number of tab-indents as its peers:

but a completely blank line is a top level parent, and whatever immediately follows (at deeper indent) descends from it.

In the following example, your expectation might be that Eta, Theta and Iota are children of the Working: project.

In fact, of course, they will not vanish and reappear with Delta Epsilon Zeta when Working: is collapsed and expanded.

(They are children of the preceding blank line)

In terms of sorting the second example above`:

  • Delta Epsilon Zeta form one sortable peer group,
  • and Eta Theta Iota, with a different parent, form a different sortable group.
1 Like

Perhaps this screenshot helps illustrate what I consider to be strange (original on left, sorted by script on right):

Also to your second point about spacing and indentation - I double checked and there are no tab-indents in my test file:

That would be a problem – structure depends on tab-indents, and none of those items would be children of Inbox without them.

The script applies one sort criterion to top level (unindented) items, and a second sort criterion to their (tab-indented) children.

If the diagram you show, Inbox: appears to have no children (only peers), so only the top-level sort criterion can be applied.

Perhaps I miss-explained. There are no tab indents in the blank lines of the test file. The screenshot shows the collapsed view of the outline, which is why no indented children tasks are shown

Got it – I’ll see if I can take a look on Sunday : -)

In the meanwhile here is a variant for you to test which:

  • Avoids use of 0 in the ordinal value dictionaries
  • Assigns a Negative Infinity value to nodes which have no known @ps value.

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // Rough draft - not for use in production.

    // New copy of front document sorted at two levels:
    // Top level by ps values
    // Second level by as values.

    // Ver 0.03
    // Rob Trew 2021
    // (MIT license and caveats)

    // Experiment:
    // - Ordinal values shifted to minimum of 1
    // - Lines without known @ps value ranked at
    //   Negative Infinity

    // ------------------- JXA CONTEXT -------------------
    // main :: IO () -> Either Message Document
    const main = () => {
        const
            taskpaper = Application("TaskPaper"),
            windows = taskpaper.windows;

        return 0 < windows.length ? (() => {
            const
                outlineText = windows.at(0).document
                .evaluate({
                    script: `${TaskPaperContext}`
                }),
                sortedDoc = new taskpaper.Document({
                    textContents: outlineText
                });

            return (
                taskpaper.documents.push(
                    sortedDoc
                ),
                sortedDoc.textContents()
            );
        })() : "No document windows open in TaskPaper";
    };

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

    // eslint-disable-next-line max-lines-per-function
    const TaskPaperContext = editor => {

        const psOrder = {
            urgent: 1,
            perpetual: 2,
            current: 3,
            following: 4,
            future: 5,
            someday: 6
        };

        const asOrder = {
            now: 1,
            next: 2,
            later: 3,
            waiting: 4,
            someday: 5
        };

        // tpMain :: () -> IO String
        const tpMain = () =>
            tabOutlineFromForest(
                fmapTree(
                    x => x.bodyString
                )(
                    distinctlySortedLevels([
                        x => psOrder[
                            x.root.getAttribute("data-ps")
                        ] || Number.NEGATIVE_INFINITY,
                        x => asOrder[
                            x.root.getAttribute("data-as")
                        ] || Number.NEGATIVE_INFINITY
                    ])(
                        pureTreeTP3(
                            editor.outline.root
                        )
                    )
                )
                .nest
            );

        // ------- DISTINCT SORTS OF UPPER LEVELS --------

        // distinctlySortedLevels ::
        // [Node -> a] -> Tree TPNode -> Tree TPNode
        const distinctlySortedLevels = accessors => {
            const go = fs => t =>
                Node(t.root)(
                    0 < fs.length ? (
                        sortOn(fs[0])(t.nest)
                        .map(go(fs.slice(1)))
                    ) : t.nest
                );

            return go(accessors);
        };

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


        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);

                return a < b ? -1 : (a > b ? 1 : 0);
            };


        // 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 = t => Node(
                f(t.root)
            )(
                t.nest.map(go)
            );

            return go;
        };


        // pureTreeTP3 :: TP3Item  -> Tree TP3Item
        const pureTreeTP3 = item => {
            const go = x =>
                Node(x)(
                    x.hasChildren ? (
                        x.children.map(go)
                    ) : []
                );

            return go(item);
        };


        // sortOn :: Ord b => (a -> b) -> [a] -> [a]
        const sortOn = f =>
            // Equivalent to sortBy(comparing(f)), but with f(x)
            // evaluated only once for each x in xs.
            // ('Schwartzian' decorate-sort-undecorate).
            xs => xs.map(
                x => [f(x), x]
            )
            .sort(comparing(x => x[0]))
            .map(x => x[1]);


        // tabOutlineFromForest :: Forest String -> String
        const tabOutlineFromForest = trees => {
            const go = tabs => tree => [
                `${tabs}${tree.root}`,
                ...tree.nest.flatMap(go(`\t${tabs}`))
            ];

            return trees.flatMap(go("")).join("\n");
        };


        // TASKPAPER CONTEXT MAIN ---
        return tpMain();
    };

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