Script: generic traffic-light cycling for tags and values


#1

A draft and customisable script which, attached to some trigger like a key-stroke, simply cycles the selected TaskPaper 3 line on to the next tag (or tag-value) in a cycle.

(If the line doesn’t have a tag which is a member of the cycling series, it gets the first tag in the series).

USE

Edit the options at the end of the script.

For example, this would cycle the selected line on to the next tag value in the (improbable) series below, cycling back to the start when it gets to the end.

{ 
    // OPTIONS:
    // Rank tags and values cool -> hot (left -> right)
    
    tagOrder: ['A', 'B', 'C'],
    valueOrder: [1, 2, 3], // this value list can be empty, for simple tags
    forward: true, // create another copy of the script for moving back (edit to `forward: false`)
    cycle: true // Extremes cycle back to start ? (edit to `cycle: false` to stop at extremes)
}
@A(1)  ->  @A(2)  ->  @A(3)  -> @B(1)  ->  @B(2)  ->  @B(3)   etc ...

Whereas:

{ 
    tagOrder: ['lo', 'med', 'hi'],
    valueOrder: []
}

would just cycle the selected line through

@lo  ->  @med  ->   @hi      (and back to @lo)

and the quadrant settings:

{ 
    tagOrder: ['trivial', 'important'],
    valueOrder: ['any time', 'urgent']
}

Would cycle through:

@trivial(any time)  >  @trivial(urgent)  > @important(any time)  > @important(urgent)     (and back to the start)

while:

{ 
    tagOrder: ['priority'],
    valueOrder: [1,2,3]
}

gives:

 @priority(1)    >   @priority(2)    >    @priority(3)

etc …

// Ver 0.12
// Rob Trew March 2015
// MIT license

(function (dctOptions) {
    'use strict';

    function tp3Context(editor, options) {

        // NEXT TAG-VALUE PAIR IN THE CYCLE ?
        // nextKeyValue :: (String, String) -> [(String, String)] 
        //      -> Bool -> Bool -> (String, String)
        function nextKeyValue(kv, kvs, blnForward, blnCycle) {
            var lstTagPrePost = matchBreak(function (a, b) {
                    return a[0] === b[0];
                }, kv, kvs),
                lstValPrePost = matchBreak(function (a, b) {
                    return a[1] === b[1];
                }, kv, lstTagPrePost[1]),
                lstTagPre = lstTagPrePost[0],
                lstPre = lstValPrePost[0],
                lstPost = lstValPrePost[1],
                lngPost = lstPost.length;

            // FORWARDS OR BACKWARDS ? CYCLING ? OR STOPPING AT EXTREMES ?
            if (blnForward) {
                if (lngPost) {
                    if (lngPost > 1) return lstPost[1]; // next warmest key:val, if any
                    else return blnCycle ? kvs[0] : kv; // or back to start
                } else return (lstPre.length ? lstPre[0] : undefined);
            } else {
                if (lstPre.length) return lstPre.slice(-1)[0]; // Previous val, if any
                else return (lstTagPre.length) ?
                    lstTagPre.slice(-1)[0] : ( // failing which, previous tag, if any
                        blnCycle ? kvs.slice(-1)[0] : kv // or back to hottest at end
                    );
            }
        }

        // SERIES SPLIT ON MATCH POINT
        // Split the list at the match:  (lstPreMatch, lstFromMatch)
        // matchBreak :: (a -> a -> Bool) -> a -> [a] -> ([a],[a])
        function matchBreak(f, x, xs) {
            return _break(function (k) {
                return f(x, k);
            }, xs);
        }

        // _break :: (a -> Bool) -> [a] -> ([a],[a])
        function _break(f, xs) {
            for (var i = 0, lng = xs.length;
                (i < lng) && !f(xs[i]); i++) {}
            return [xs.slice(0, i), xs.slice(i)];
        }

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

        //  intersect :: [a] -> [a] -> [a]
        function intersect(xs, ys) {
            return xs.length && ys.length ? (
                xs.filter(function (x) {
                    return ys.indexOf(x) !== -1;
                })
            ) : [];
        }

        // tagForm :: (String, String) -> String
        function tagForm(lstKV) {
            var strVal = lstKV[1];

            return '@' + lstKV[0].replace(/^data-/, '') +
                (strVal.length ? '(' + strVal + ')' : '');
        }

        // TASKPAPER CONTEXT MAIN:

        // WHAT IS THE FULL CYCLE OF TAG VALUES ?
        var lstTags = options.tagOrder.map(function (x) {
                return 'data-' + x;
            }),
            lstVals = options.valueOrder,
            lstTagVals = lstVals.length ? lstVals.map(function (x) {
                return x.toString();
            }) : [''],
            lstSeries = concatMap(lstTags, function (k) {
                return concatMap(lstTagVals, function (v) {
                    return [[k, v]];
                })
            });


        // WHAT TAGS (FROM THIS CYCLE) DOES THE FIRST SELECTED LINE HAVE ?
        var sln = editor.selection.startItem,
            lstAttribs = sln.attributeNames,
            lstKeys = intersect(lstTags, lstAttribs);

        if (lstKeys.length > 0) {
            // IF THE LINE HAS MORE THAN ONE CYCLE TAG, 
            // WHICH IS THE HOTTEST (LATEST STAGE OF THE CYCLE) ?
            var lstKeyVals = lstKeys.length ? lstKeys.map(function (k) {
                    return [k, sln.getAttribute(k)];
                }) : undefined,

                kvWarmest = lstKeyVals ? lstKeyVals
                .sort(function (a, b) {
                    var x = lstTags.indexOf(a[0]),
                        y = lstTags.indexOf(b[0])

                    if (x < y) return -1;
                    if (x > y) return 1;

                    var x2 = lstTagVals.indexOf(a[1]),
                        y2 = lstTagVals.indexOf(b[1]);

                    return x2 < y2 ? -1 : x2 > y2 ? 1 : 0
                })
                .slice(-1)[0] : undefined;


            // AND WHICH TAG COMES NEXT IN THE CYCLE ?
            var kvNext = nextKeyValue(
                kvWarmest, lstSeries, options.forward, options.cycle
            );
        } else {
            // NOTHING FOUND, SO START AT ONE END OF THE CYCLE
            var kvNext = options.forward ? (
                lstSeries[0]
            ) : lstSeries.slice(-1)[0];
        }

        if (kvNext) {
            // UPDATE THE FIRST SELECTED LINE TO THE NEXT TAG IN THE CYCLE

            editor.outline.groupUndoAndChanges(function () {
                // CLEAR ANY TAGS ON THE LINE THAT BELONG TO THIS SERIES,
                lstKeys.forEach(function (k) {
                    return sln.removeAttribute(k);
                });
                // AND REPLACE THEM WITH THE NEXT @TAG(VALUE) IN THE SERIES
                sln.setAttribute(kvNext[0], kvNext[1]);
            });
            return tagForm(kvNext);
        }
    }

    var ds = Application("TaskPaper")
        .documents,
        varResult = ds.length ? ds[0].evaluate({
            script: tp3Context.toString(),
            withOptions: dctOptions
        }) : false;

    return varResult;
    
})({ 
    // OPTIONS:
    // Rank tags and values cool -> hot (left -> right)
    
    tagOrder: ['A', 'B', 'C'],
    valueOrder: [1, 2, 3], // this value list can be empty, for simple tags
    forward: true, // create another copy of the script for moving back (edit to `forward: false`)
    cycle: true // Extremes cycle back to start ? (edit to `cycle: false` to stop at extremes)
});

Scripting question: changing @due to @today if the date is today