Script :: Toggle spacing after outline sections

For print, deleting blank rows in an outline can be a good way of avoiding empty bullet points.

For working on the screen, however, the structure of an outline may be clearer at a glance if outline sections are separated by a blank line.

Here draft script which alternates between:

  1. Pruning out empty childless rows in the visible part of the document, and (either only in selected rows, or in all visible rows if the selection is not extended)
  2. adding an empty row after each trailing leaf (each leaf which is the last of its siblings.
Expand disclosure triangle to view animation

See play button at bottom right
ScreenFlow


To test in Script Editor, set language selector at top left to JavaScript rather than AppleScript

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

    // TOGGLE EMPTY ROWS IN BIKE OUTLINE

    // EITHER Delete any childless empty rows in the
    // visible part of the front document.
    //
    // OR (if no childless empty rows are seen)
    // Add blank row after each trailing leaf row.
    //
    // i.e. after any leaf row which is the last
    // of its siblings.

    // If the SELECTION IS EXTENDED,
    // then only the selected range of lines is affected
    //
    // Otherwise, *all* rows in any visible
    // part of the document are affected out.

    // Addition and pruning of spaces both
    // reversible with ⌘Z

    // Rob Trew @2022
    // Ver 0.05

    // const main :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return doc.exists() ? (() => {
            const
                selectionExtended = Boolean(
                    doc.selectedText()
                ),
                childlessEmptyRows = doc.rows.where((
                    selectionExtended ? (
                        inSelection
                    ) : (x => x)
                )({
                    _and: [
                        {visible: true},
                        {name: ""},
                        {containsRows: false}
                    ]
                }));

            return 0 < childlessEmptyRows.length ? (
                // EITHER prune out blank rows
                pruneRows(bike)(doc)(selectionExtended)(
                    childlessEmptyRows
                )
                // OR add blank rows after trailing leaves.
            ) : spaceAfterTrailingLeaves(bike)(doc);
        })() : "No documents open in Bike";
    };

    // pruneRows :: Application -> Document -> Bool ->
    // Rows -> IO String
    const pruneRows = bike =>
        doc => selectionExtended => childlessEmptyRows => {
            const n = childlessEmptyRows.length;

            return (
            // Effect
                bike.delete(childlessEmptyRows),
                // Value (message string)
                [
                    [`Deleted ${n} empty rows in`],
                    selectionExtended ? (
                        ["extended selection of "]
                    ) : [],
                    [`document: "${doc.name()}"`],
                    0 < n ? ["(⌘Z to undo)"] : []
                ]
                .flat()
                .join("\n")
            );
        };


    // spaceAfterTrailingLeaves :: Application ->
    // Document -> IO String
    const spaceAfterTrailingLeaves = bike =>
        doc => {
            const
                isVisibleLeaf = {
                    _and: [
                        {visible: true},
                        {_not: [{containsRows: true}]}
                    ]
                },
                leaves = doc.rows.where(
                    Boolean(doc.selectedText()) ? (
                        inSelection(isVisibleLeaf)
                    ) : isVisibleLeaf),
                lastLeaves = leaves().filter(
                    x => null === x.nextSiblingRow()
                ),
                n = lastLeaves.length;

            return (
                lastLeaves.forEach(
                    x => x.containerRow.rows.push(
                        new bike.Row({name: ""})
                    )
                ),
                `Added ${n} blank rows, to space sections.`
            );
        };


    // inSelecton :: Dict -> Dict
    const inSelection = match =>
    // JXA Where/Whose condition for Bike Rows
    // further restricted to selected rows only.
        ({
            _and: [
                {selected: true},
                match
            ]
        });

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


Keyboard Maestro version:

2 Likes

Thinking of reimplementing this, for Bike 2 Preview, in terms of paths / styles, so that any final leaf, ie:

leaf which has no following siblings

is styled with some additional following space (for clearer visual separation of different parts of the outline)

(The Bike 1 script above adds or removes blank lines after leaves which have no following siblings)

One possible outline path might, I think, be:

//* except (//*/parent::* union //*/preceding-sibling::*)

(anything except parents and preceding siblings)

but I wonder if I’m missing something lighter or more direct ?

I have a bunch of escape hatch functions.

Look in the updated outline paths guide in the last “Functions Reference” section.

Using them I think this does what you want:

//leaf() = true and last-child() = true

I think most of the time it won’t matter, but using these functions is also much faster in most cases. They just perform a direct test on the outline model instead of querying a full axis into a set and then performing set operations.

Edit I also just realized that at some point recently I broke the Outline Path Explorer window. Will look into that for next release.

1 Like

I had missed that powerful set of functions – thank you !

1 Like

One more note.

You can also do something similar with stylesheets. So instead of a script that inserts the empty lines you could just setup a style that adds extra padding after these last sibling rows.

defineRowRule(".leaf() = true and last-child() = true", (env, row) => {
    row.padding.bottom += 100
})
1 Like