Script :: Move highlight format to next row

If we use highlight format to mark a focal row in a list,
we may at some point want to move that highlight on the next row
(clearing it from the current row).

Here is a script which cycles highlighting through the set of sibling rows which contains the selection cursor.

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

    // -------- MOVE HIGHLIGHT FORMAT TO NEXT ROW --------

    // Highlighting cycled through the peer rows which
    // contain the selection cursor.
    // The highlighting advances each time the script is run,
    // and cycles back to the first sibling afer the last.

    // Rob Trew @2024
    // Ver 0.2

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

        return either(
            alert("Highlight cycled among siblings.")
        )(
            focalName => focalName
        )(
            bindLR(
                frontDoc.exists()
                    ? Right(frontDoc.selectionRow())
                    : Left("No document open in Bike.")
            )(
                highlightCycledLR
            )
        );
    };


    // highlightCycledLR :: Bike Row -> Either String String
    const highlightCycledLR = selnRow => {
        // Either a message, or the name of a row which
        // has newly received the highlighting focus
        // in a cycle among the set of peer rows which
        // contains the selection.
        const parentRow = selnRow.containerRow;

        return bindLR(
            nextMarkedSiblingLR(parentRow.rows)
        )(
            nextRow => moveHighlightToChildByIdLR(
                parentRow
            )(
                nextRow.id()
            )
        );
    };


    // ------------- HIGHLIGHTING FUNCTIONS --------------

    // hasMarkedText :: Row -> Bool
    const hasMarkedText = row =>
        // True only if any attribute run in the row
        // is highlighted.
        0 < row.textContent.attributeRuns.where({
            highlight: true
        })
        .length;


    // highlightClearedOrSet :: String -> Row -> IO [String]
    const highlightClearedOrSet = focalId =>
        // Highlight set in the row if it has the focal ID,
        // otherwise any highlighting cleared from it.
        row => {
            const
                isMarked = focalId === row.id(),
                runs = row.textContent.attributeRuns;

            return (
                enumFromTo(0)(runs.length - 1)
                .forEach(
                    i => runs.at(i).highlight = isMarked
                ),
                isMarked
                    ? [row.name()]
                    : []
            );
        };


    // moveHighlightToChildByIdLR :: Row ->
    // String -> Either String String
    const moveHighlightToChildByIdLR = parentRow =>
        // Either a message or the name of the row
        // which matches the given id, and has now been
        // highlit, with any highlighting cleared from
        // its peers.
        focalChildId => {
            const
                markedNames = parentRow.rows.where({
                    _not: [{name: {_beginsWith: "="}}]
                })()
                .flatMap(
                    highlightClearedOrSet(focalChildId)
                );

            return 0 < markedNames.length
                ? Right(markedNames[0])
                : Left(
                    [
                        "Child not found for highlighting",
                        `by id: "${focalChildId}"`
                    ]
                    .join("\n")
                );
        };


    // nextMarkedSiblingLR :: Rows ->
    // Either String Row
    const nextMarkedSiblingLR = peerRows => {
        // Either a message or the next peer row
        // that in the highlighting cycle.
        // (The first peer if the last is highlit).
        const
            n = peerRows.length,
            i = peerRows().findIndex(hasMarkedText);

        return 0 < n
            ? Right(
                peerRows.at(
                    -1 !== i
                        ? (1 + i) % n
                        : 0
                )
            )
            : Left("Empty collection of peers.");
    };


    // ----------------------- 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"
                }),
                s
            );
        };

    // --------------------- GENERIC ---------------------

    // 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 = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr
            ? lr
            : mf(lr.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 => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = m =>
    // Enumeration of the integers from m to n.
        n => Array.from(
            {length: 1 + n - m},
            (_, i) => m + i
        );


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

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

See: Using Scripts | Bike

3 Likes