How to "bulk tag"

I see no end to this process, so I’ll probably have to pause there, alas, and get on with other things :-)

It should be possible, however, to learn enough to do this kind of experimentation and discovery yourself.

The trick is just to learn to think concretely and specifically about the values that you are trying to define.

You’ve been more than generous, and I appreciate it.

1 Like

Won’t have time to update or amend this one, and no guarantees,

(in particular not at all sure that I can parse your meaning in the phrase enter the last Column name … perhaps you mean that you want memory of the MRU tag value ? The column name is fixed anyway, as Tag as far as I am aware … )

but you could try this:

add-tags-in-subtree.omnijs 2.zip (2.0 KB)

JS Source
/*{
    "type": "action",
    "name": "Append string to Tags cell for all descendants",

    "author": "Rob Trew",
    "version": "0.10",
    "description": "Append string to Tags cell for all descendants",
    "mediumLabel": "Batch tag",
    "paletteLabel": "Batch tag",
}*/
(() => {
    let defaultTag = '';
    return Object.assign(
        new PlugIn.Action(selection => {
            'use strict';

            // main :: IO ()
            const main = () => {
                // List of fields,
                [
                    new Form.Field.String(
                        'colValue',
                        'Value:',
                        defaultTag
                    )
                ]
                // consolidated into a form,
                .reduce(
                        (frm, fld) => (
                            frm.addField(fld),
                            frm
                        ),
                        new Form()
                    )
                    // and displayed.
                    .show(
                        'Value for empty tag cells:',
                        'OK'
                    )
                    // With processing of any form values.
                    .then(result => {
                        const
                            dteStart = new Date(),
                            tagString = result.values.colValue,

                            n = itemsTagged(
                                columnFoundOrCreated(Column.Type.Text)('Tag')
                            )(tagString)(
                                document.editors[0].selection.items
                            ),

                            plural = (1 !== n) ? 's' : '',
                            elapsed = (new Date() - dteStart) / 1000;
                        new Alert(
                            'Subtree tagging',
                            `${n} '${tagString}' tag${plural} ` + (
                                `applied in ${elapsed} seconds.`
                            )
                        ).show();
                        defaultTag = tagString;
                    });
            };

            // ---------------- OO EFFECTS -----------------

            // columnFoundOrCreated :: ColumnType -> 
            // String -> Column
            const columnFoundOrCreated = colType =>
                colName => {
                    const outline = document.outline;
                    return outline.columns.byTitle(
                        colName
                    ) || outline.addColumn(
                        colType,
                        document.editors[0].afterColumn(null),
                        col => col.title = colName
                    );
                };

            // itemsTagged :: Column -> String -> 
            // [OOItem] -> Int
            const itemsTagged = col =>
                tag => items => commonAncestors(items)
                .flatMap(
                    x => [x].concat(
                        x.descendants
                    ).flatMap(x => {
                        const
                            v = x.valueForColumn(col),
                            sv = null !== v ? (
                                v.string
                            ) : '';
                        return !sv.includes(tag) ? [(
                            x.setValueForColumn(
                                sv ? (
                                    sv + ' ' + tag
                                ) : tag,
                                col
                            ),
                            1
                        )] : [];
                    })
                ).length;

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

            // commonAncestors :: [OOItem] -> [OOItem]
            const commonAncestors = items => {
                // Only items which do not descend
                // from other items in the list.
                const
                    dct = items.reduce(
                        (a, x) => (a[x.identifier] = true, a), {}
                    );
                return items.filter(
                    x => !until(
                        y => (null === y) || dct[y.identifier]
                    )(v => v.parent)(x.parent)
                );
            };

            // showLog :: a -> IO ()
            const showLog = (...args) =>
                console.log(
                    args
                    .map(JSON.stringify)
                    .join(' -> ')
                );

            // until :: (a -> Bool) -> (a -> a) -> a -> a
            const until = p =>
                // Iterative application of f to x 
                // until the resulting value matches p.
                f => x => {
                    let v = x;
                    while (!p(v)) v = f(v);
                    return v;
                };

            // MAIN ---
            return main();
        }), {
            validate: selection => 0 < selection.items.length
        });
})();

Wow. That did it.

(And what I meant by “enter the last column name” was in reference to the tag-subtree script, not this script.)

Many, many thanks! Put yourself in for a raise.

Updated here and in the post above:

  • Now creates a Tag column if none is found in the front document,
  • and holds the MRU tag value memory in a more sensible scope.

add-tags-in-subtree.omnijs.zip (2.0 KB)

JS Source
/*{
    "type": "action",
    "name": "Append string to Tags cell for all descendants",

    "author": "Rob Trew",
    "version": "0.10",
    "description": "Append string to Tags cell for all descendants",
    "mediumLabel": "Batch tag",
    "paletteLabel": "Batch tag",
}*/
(() => {
    // -------------------- MRU MEMORY ---------------------
    let defaultTag = '';

    // ---------------------- PLUGIN -----------------------
    return Object.assign(
        new PlugIn.Action(selection => {
            'use strict';

            // main :: IO ()
            const main = () => {
                // List of fields,
                [
                    new Form.Field.String(
                        'colValue',
                        'Value:',
                        defaultTag
                    )
                ]
                // consolidated into a form,
                .reduce(
                        (frm, fld) => (
                            frm.addField(fld),
                            frm
                        ),
                        new Form()
                    )
                    // and displayed.
                    .show(
                        'Value for empty tag cells:',
                        'OK'
                    )
                    // With processing of any form values.
                    .then(result => {
                        const
                            dteStart = new Date(),
                            tagString = result.values.colValue,

                            n = itemsTagged(
                                columnFoundOrCreated(Column.Type.Text)('Tag')
                            )(tagString)(
                                document.editors[0].selection.items
                            ),

                            plural = (1 !== n) ? 's' : '',
                            elapsed = (new Date() - dteStart) / 1000;

                        // ------------ RESULTS ------------
                        new Alert(
                            'Subtree tagging',
                            `${n} '${tagString}' tag${plural} ` + (
                                `applied in ${elapsed} seconds.`
                            )
                        ).show();
                        defaultTag = tagString;
                    });
            };

            // ---------------- OO EFFECTS -----------------

            // columnFoundOrCreated :: ColumnType -> 
            // String -> Column
            const columnFoundOrCreated = colType =>
                colName => {
                    const outline = document.outline;
                    return outline.columns.byTitle(
                        colName
                    ) || outline.addColumn(
                        colType,
                        document.editors[0].afterColumn(null),
                        col => col.title = colName
                    );
                };

            // itemsTagged :: Column -> String -> 
            // [OOItem] -> Int
            const itemsTagged = col =>
                tag => items => commonAncestors(items)
                .flatMap(
                    x => [x].concat(x.descendants)
                    .flatMap(x => {
                        const
                            v = x.valueForColumn(col),
                            sv = null !== v ? (
                                v.string
                            ) : '';
                        return !sv.includes(tag) ? [(
                            x.setValueForColumn(
                                sv ? (
                                    sv + ' ' + tag
                                ) : tag,
                                col
                            ),
                            1
                        )] : [];
                    })
                ).length;

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

            // commonAncestors :: [OOItem] -> [OOItem]
            const commonAncestors = items => {
                // Only items which do not descend
                // from other items in the list.
                const
                    dct = items.reduce(
                        (a, x) => (a[x.identifier] = true, a), {}
                    );
                return items.filter(
                    x => !until(
                        y => (null === y) || dct[y.identifier]
                    )(v => v.parent)(x.parent)
                );
            };

            // showLog :: a -> IO ()
            const showLog = (...args) =>
                console.log(
                    args
                    .map(JSON.stringify)
                    .join(' -> ')
                );

            // until :: (a -> Bool) -> (a -> a) -> a -> a
            const until = p =>
                // Iterative application of f to x 
                // until the resulting value matches p.
                f => x => {
                    let v = x;
                    while (!p(v)) v = f(v);
                    return v;
                };

            // MAIN ---
            return main();
        }), {
            validate: selection => 0 < selection.items.length
        });
})();

Can I send you a private message on a script I need?