Batch-adding tag(s) to selection

Possibly a trivial question… Is there a plugin or script for adding one or more tags to a selection of lines? I have found Rob Trew’s script for batch deletion but no counterpart for adding. Thank you in advance.

If you simply need to add plain tags like @today, (without a value) then you could test the following draft.

( I have followed it with a newer batch tag-deletion script).

Let me know more, however, if you would like to batch-add @tag(value)

Draft script 1 of 2 (batch-adding tags)

// *ADD* ONE OR MORE TAGS TO SELECTED LINE(S)
// ver 0.01
// 
// CHOOSE tag(s) from a sorted menu listing:
// 1. Any tags already found in the document
// 2. Any default tags specified at the end of this script

// Rough draft: Rob Trew Dec 2016. MIT license

// Rough draft: test before use
// Author: Rob Trew Dec 2016. MIT license

(function (dctOptions) {
    'use strict';

    // FT CONTEXT MAIN (1 of 2)

    function fnTagSet(editor, options) {

        // GENERIC FUNCTIONS

        // concatMap :: (a -> [b]) -> [a] -> [b]
        function concatMap(f, xs) {
            return [].concat.apply([], xs.map(f));
        }

        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        function sortBy(f, xs) {
            return xs.sort(f);
        }

        // nubBy :: (a -> a -> Bool) -> [a] -> [a]
        function nubBy(fnEq, lst) {
            var x = lst.length ? lst[0] : null;

            return x ? [x].concat(
                nubBy(
                    fnEq,
                    lst.slice(1)
                    .filter(function (y) {
                        return !fnEq(x, y);
                    })
                )
            ) : [];
        }

        // Tags in document (sorted, unique)

        var rs = editor.selectedRanges();

        return {
            allTags: sortBy(function (a, b) {
                return a < b ? -1 : (a > b ? 1 : 0);
            }, nubBy(function (x, y) {
                    return x === y;
                }, options.defaultTags
                .concat(concatMap(function (n) {
                        return Object.keys(n.tags());
                    }, editor.tree()
                    .nodes())))),

            nodeIDs: concatMap(function (r) {
                return concatMap(function (n) {
                    return n.id;
                }, r.nodesInRange());
            }, editor.selectedRanges())
        }

    }


    // FT CONTEXT MAIN (2 of 2)


    function fnDocUpdated(editor, options) {
        var lstAdd = options.tagsChosen,
            dctAdded = {};

        
        if (lstAdd.length > 0) {
            var tree = editor.tree();

            tree.beginUpdates();

            options.nodeIDs.forEach(function (id) {
                var oNode = tree.nodeForID(id);

                lstAdd.forEach(function (s) {
                    if (!oNode.hasTag(s)) {
                        oNode.addTag(s);
                        
                        dctAdded[s] = (dctAdded[s] || 0) + 1;
                    }
                });
            });

            tree.endUpdates();
        }
        return dctAdded;
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    var ds = Application("FoldingText")
        .documents,
        d = ds.length ? ds[0] : undefined,
        dctTags = d ? d.evaluate({
            script: fnTagSet.toString(),
            withOptions: dctOptions
        }) : [],
        lstTags = dctTags.allTags || {},
        lngTags = lstTags.length;

    if (lngTags > 0) {
        if (lngTags > 1) {
            var a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a);

            sa.activate();


            // OFFER CHOICE OF TAGS TO ADD (IF MULTIPLE)
            var varChoice = sa.chooseFromList(lstTags, {
                    withTitle: "Add tags to selected lines",
                    withPrompt: 'Tag(s) to add:',
                    defaultItems: lstTags[0],
                    okButtonName: 'OK',
                    cancelButtonName: 'Cancel',
                    multipleSelectionsAllowed: true,
                    emptySelectionAllowed: true
                }),

                lstAdd = varChoice ? varChoice : [],
                lngAdd = lstAdd.length;

        } else {
            var lngAdd = 1,
                lstAdd = lstTags;
        }

        // ADD ANY CHOSEN TAGS TO SELECTED LINES
        if (lngAdd > 0) {
            return d.evaluate({
                script: fnDocUpdated.toString(),
                withOptions: {
                    tagsChosen: lstAdd,
                    nodeIDs: dctTags.nodeIDs
                }
            });
        }
    }
})({
    defaultTags: ['due', 'alert', 'today']
});

Draft script 2 of 2: batch-deleting tags


// *DELETE* ONE OR MORE TAGS FROM SELECTED LINE(S)
// ver 0.20
// If there is more than one tag,
// choose tag(s) to clear from a menu


// Rough draft: test before use
// Author: Rob Trew Dec 2016. MIT license

(function () {
    'use strict';

    // FT CONTEXT MAIN (1 of 2)

    function fnTagSet(editor, options) {

        // GENERIC FUNCTIONS

        // concatMap :: (a -> [b]) -> [a] -> [b]
        function concatMap(f, xs) {
            return [].concat.apply([], xs.map(f));
        }

        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        function sortBy(f, xs) {
            return xs.sort(f);
        }

        // nubBy :: (a -> a -> Bool) -> [a] -> [a]
        function nubBy(fnEq, lst) {
            var x = lst.length ? lst[0] : null;

            return x ? [x].concat(
                nubBy(
                    fnEq,
                    lst.slice(1)
                    .filter(function (y) {
                        return !fnEq(x, y);
                    })
                )
            ) : [];
        }

        // Tags in selected nodes (sorted, unique)

        var rs = editor.selectedRanges();

        return {
            tags: sortBy(function (a, b) {
                return a < b ? -1 : (a > b ? 1 : 0);
            }, nubBy(function (x, y) {
                return x === y;
            }, concatMap(function (r) {
                return concatMap(function (n) {
                    return Object.keys(n.tags());
                }, r.nodesInRange());
            }, rs))),
            
            nodeIDs: concatMap(function (r) {
                return concatMap(function (n) {
                    return n.id;
                }, r.nodesInRange());
            }, rs)
        }

    }


    // FT CONTEXT MAIN (2 of 2)


    function fnDocUpdated(editor, options) {
        var lstClear = options.tagsChosen,
            dctCleared = {};

        if (lstClear.length > 0) {
            var tree = editor.tree();

            tree.beginUpdates();

            options.nodeIDs.forEach(function (id) {
                var oNode = tree.nodeForID(id);

                lstClear.forEach(function (s) {
                    if (oNode.hasTag(s)) {
                        oNode.removeTag(s);

                        dctCleared[s] = (dctCleared[s] || 0) +
                            1;
                    }
                });
            });

            tree.endUpdates();
        }
        return dctCleared;
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    var ds = Application("FoldingText")
        .documents,
        d = ds.length ? ds[0] : undefined,
        dctSeln = d ? d.evaluate({
            script: fnTagSet.toString(),
        }) : [],
        lstTags = dctSeln.tags || {},
        lngTags = lstTags.length;

    if (lngTags > 0) {
        if (lngTags > 1) {
            var a = Application.currentApplication(),
                sa = (a.includeStandardAdditions = true, a);

            sa.activate();


            // OFFER CHOICE OF TAGS TO CLEAR (IF MULTIPLE)
            var varChoice = sa.chooseFromList(lstTags, {
                    withTitle: "Clear tags from selected lines",
                    withPrompt: 'Tag(s) to clear:',
                    defaultItems: lstTags[0],
                    okButtonName: 'OK',
                    cancelButtonName: 'Cancel',
                    multipleSelectionsAllowed: true,
                    emptySelectionAllowed: true
                }),

                lstClear = varChoice ? varChoice : [],
                lngClear = lstClear.length;

        } else {
            var lngClear = 1,
                lstClear = lstTags;
        }


        //return [lngClear, lstClear];

        // CLEAR ANY SELECTED TAGS
        if (lngClear > 0) {
            return d.evaluate({
                script: fnDocUpdated.toString(),
                withOptions: {
                    tagsChosen: lstClear,
                    nodeIDs: dctSeln.nodeIDs
                }
            });
        }
    }
})();

Very generous of you, @complexpoint, and customarily excellent. Thank you! This will do — I don’t need value tags right now.