Scripts to set priority, and sort a project by priority


#21

Sorry about the separate responds. Using a phone right now.

Currently I could use an example for either. My previous request was kind of solved by just keeping categories of information in the same file. That results in bigger files. If it is not much to ask, could you provide example for both?


#22

Starting with something simple - reports which just sort and group a defined set of items as a flat list (without indented descendants), here is a draft in which you can set some options (at the foot of the script), including the path of the output file to write to.

This version is for the front TaskPaper document in the running app.

I’m not sure how self-explanatory the options at the end are, but note that the orderBy field, needs to be a [ list ] (JS array), even if it contains only one string. The first string specifies primary sort key and direction, and any further strings can specify secondary and tertiary etc sorts.

To avoid grouping, edit the groupBy field to an empty string.

(Test with dummy data from JavaScript mode of Script Editor, or from an Execute JXA action in Keyboard Maestro, etc)

// (c) 2017 Rob Trew
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Write sorted and optionally grouped report from TaskPaper to new file
// VER 0.03

// Sample report spec (see end of script):
// {
//     outPutFile: '~/Desktop/report.taskpaper',
//     itemsInPath: '/@type=project except Archive',
//     orderBy: [
//         '@priority descending as number',
//         '@effort descending as number',
//     ],
//     groupBy: '@priority as number descending'
// }


(dctOptions => {
    'use strict';

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = (editor, options) => {

        // GENERIC FUNCTIONS -------------------------------------------------

        // compare :: a -> a -> Ordering
        const compare = (a, b) => a < b ? -1 : (a > b ? 1 : 0);

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // concatMap :: (a -> [b]) -> [a] -> [b]
        const concatMap = (f, xs) =>
            xs.length > 0 ? [].concat.apply([], xs.map(f)) : [];

        // Handles two or more arguments
        // curry :: ((a, b) -> c) -> a -> b -> c
        const curry = (f, ...args) => {
            const go = xs => xs.length >= f.length ? (f.apply(null, xs)) :
                function () {
                    return go(xs.concat(Array.from(arguments)));
                };
            return go([].slice.call(args));
        };

        // drop :: Int -> [a] -> [a]
        // drop :: Int -> String -> String
        const drop = (n, xs) => xs.slice(n);

        // elem :: Eq a => a -> [a] -> Bool
        const elem = (x, xs) => xs.indexOf(x) !== -1;

        // elems :: Dict -> [a]
        const elems = Object.values;

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = (a, b) => a === b;

        // foldl :: (a -> b -> a) -> a -> [b] -> a
        const foldl = (f, a, xs) => xs.reduce(f, a);

        // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
        const groupBy = (f, xs) => {
            const dct = xs.slice(1)
                .reduce((a, x) => {
                    const h = a.active.length > 0 ? a.active[0] : undefined;
                    return h !== undefined && f(h, x) ? {
                        active: a.active.concat([x]),
                        sofar: a.sofar
                    } : {
                        active: [x],
                        sofar: a.sofar.concat([a.active])
                    };
                }, {
                    active: xs.length > 0 ? [xs[0]] : [],
                    sofar: []
                });
            return dct.sofar.concat(
                dct.active.length > 0 ? [dct.active] : []
            );
        };

        // intercalate :: String -> [String] -> String
        const intercalate = (s, xs) => xs.join(s);

        // isNull :: [a] -> Bool
        // isNull :: String -> Bool
        const isNull = xs =>
            Array.isArray(xs) || typeof xs === 'string' ? (
                xs.length < 1
            ) : undefined;

        // isPrefixOf :: [a] -> [a] -> Bool
        const isPrefixOf = (xs, ys) => {
            const pfx = (xs, ys) => xs.length ? (
                ys.length ? xs[0] === ys[0] && pfx(
                    xs.slice(1), ys.slice(1)
                ) : false
            ) : true;
            return typeof xs !== 'string' ? pfx(xs, ys) : ys.startsWith(xs);
        };

        // length :: [a] -> Int
        const length = xs => xs.length;

        // map :: (a -> b) -> [a] -> [b]
        const map = (f, xs) => xs.map(f);

        // mappendComparing :: [((a -> b), Bool)] -> (a -> a -> Ordering)
        const mappendComparing = fboolPairs =>
            (x, y) => fboolPairs.reduce(
                (ord, [f, b]) => ord !== 0 ? (
                    ord
                ) : (
                    b ? compare(f(x), f(y)) : compare(f(y), f(x))
                ), 0
            );

        // sortBy(on(compare,length), xs)
        // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
        const on = (f, g) => (a, b) => f(g(a), g(b));

        // show :: Int -> a -> Indented String
        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[1], null, x[0]] : x
            );

        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        const sortBy = (f, xs) =>
            xs.slice()
            .sort(f);

        // tailDef :: [a] -> [a]
        const tailDef = xs => xs.length > 0 ? xs.slice(1) : [];

        // toLower :: String -> String
        const toLower = s => s.toLowerCase();

        // toTitle :: String -> String
        const toTitle = s =>
            (s.length ? s[0].toUpperCase() + s.slice(1)
                .toLowerCase() : '');

        // unlines :: [String] -> String
        const unlines = xs => xs.join('\n');

        // unwords :: [String] -> String
        const unwords = xs => xs.join(' ');

        // words :: String -> [String]
        const words = s => s.split(/\s+/);

        // N-ARY SORTS BY ASCENDING OR DESCENDING KEYS -------------------

        // Dictionary of sort features -> (getFunction, Ascending?) pair
        // customSort :: {key::String, asc::Bool, type::JS Class} ->
        //              ((Item -> a), Bool)
        const customSort = dct => [
            propValFn(dct.key, dct.type), dct.asc
        ];

        // Blacklist -> Attributes dictionary -> String of remaining tags
        // exceptTags :: [String] -> Dict -> String
        const exceptTags = (exclns, dct) =>
            unwords(concatMap(
                dk => {
                    const
                        k = isPrefixOf('data-', dk) ? drop(5, dk) : '',
                        v = dct[dk];
                    return (isNull(k) || elem(k, exclns)) ? [] : [
                        '@' + k + (v !== undefined ? '(' + v + ')' : '')
                    ];
                }, Object.keys(dct)
            ));

        // propValFn :: {key::String, asc::Bool, type::JS Class}
        //                      -> (Item -> a)
        const propValFn = (k, type) =>
            item => ((k !== 'text') ? (
                item.getAttribute('data-' + k, type)
            ) : item.bodyContentString);

        const groupValFn = (k, type) =>
            group => ((k !== 'text') ? (
                group[0].getAttribute('data-' + k, type)
            ) : group[0].bodyContentString);

        // pure :: a -> f a
        const pure = x => [x];

        // specParse :: String -> (String, Bool, maybe JS Class)
        const specParse = s =>
            foldl((a, k, i) => {
                    return k[0] !== '@' ?
                        elem(
                            k, ['asc', 'desc', 'ascending', 'descending']
                        ) ? (
                            a.asc = k[0] === 'a',
                            a
                        ) : (
                            elem(k, ['date', 'num', 'number']) ? (
                                a.type = ((k[0] !== 'd') ? Number : Date),
                                a
                            ) : a // Unknown word - ignored
                        ) : (
                            a.key = tailDef(k), // @key
                            a
                        );
                }, {
                    key: 'text',
                    asc: true,
                    type: undefined
                },
                words(toLower(s))
            );

        const
            outline = editor.outline,
            itemSortFns = map(
                ks => customSort(specParse(ks)),
                options.orderBy
            ),
            mbGroup = options.groupBy && (options.groupBy.length > 1) ? {
                nothing: false,
                just: specParse(options.groupBy)
            } : {
                nothing: true
            },
            groupProp = mbGroup.nothing ? (
                _ => ''
            ) : propValFn(mbGroup.just.key, mbGroup.just.type),

            // Grouping function - single group if no property specified
            // groupByPropVal :: [a] -> [[a]]
            groupByPropVal = mbGroup.nothing ? (
                pure
            ) : curry(groupBy)(
                on(eq, propValFn(mbGroup.just.key, mbGroup.just.type))
            );

        // itemReport :: Bool -> [String] -> TP3 Item -> String
        const itemReport = (blnFull, excludedTags, item) =>
            blnFull ? (
                item.bodyString
            ) : item.bodyContentString;

        // groupReport :: {key::String, asc::Bool, type:JS type} ->
        //                      [[TP3Item]] -> String
        const groupReport = (mbGroup, groups) =>
            mbGroup.nothing ? (
                unlines(map(
                    item => itemReport(true, [], item),
                    groups[0]
                ))
            ) : (() => {
                const
                    dct = mbGroup.just,
                    k = dct.key,
                    pf = propValFn(k, dct.type);
                return unlines(map(gp => {
                        const v = pf(gp[0]);
                        return (v !== undefined ? (
                                toTitle(k) + '(' + v + ')'
                            ) : '(' + k + ' undefined)') + '\n' +
                            unlines(
                                map(
                                    x => {
                                        const strType = x.getAttribute(
                                            'data-type'
                                        );
                                        return (strType === 'task' ? (
                                                '- '
                                            ) : '') +
                                            x.bodyContentString +
                                            (strType === 'project' ? (
                                                ': '
                                            ) : ' ') +
                                            exceptTags(
                                                ['type', k],
                                                x.attributes
                                            )
                                    },
                                    gp
                                )
                            ) + '\n';
                    },
                    groups
                ));
            })();

        // return unlines(map(gp => gp[0].bodyString,
        const groups = groupByPropVal(
            sortBy(
                mappendComparing(itemSortFns),
                outline.evaluateItemPath(
                    options.itemsInPath
                )
            )
        );

        return groupReport(mbGroup, mbGroup.nothing ? (
            groups
        ) : sortBy(
            mappendComparing([
                [groupValFn(mbGroup.just.key, mbGroup.just.type), mbGroup.asc]
            ]),
            groups
        ));
    };

    // JXA CONTEXT------------------------------------------------------------

    // standardPath :: String -> Path
    const standardPath = strPath =>
        Path(ObjC.unwrap($(strPath)
            .stringByStandardizingPath));

    // isNull :: [a] -> Bool
    // isNull :: String -> Bool
    const isNull = xs =>
        Array.isArray(xs) || typeof xs === 'string' ? (
            xs.length < 1
        ) : undefined;

    // strip :: String -> String
    const strip = s => s.trim();

    // writeFileMay :: FilePath -> String -> Maybe FilePath
    const writeFileMay = (strPath, strText) => {
        const
            e = $(),
            fullPath = $(strPath)
            .stringByStandardizingPath;
        return $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                fullPath, false,
                $.NSUTF8StringEncoding, e
            ) ? {
                nothing: false,
                just: ObjC.unwrap(fullPath)
            } : {
                nothing: true
            };
    };

    const
        tp3 = Application('TaskPaper'),
        ds = tp3.documents,
        mbd = ds.length > 0 ? {
            just: ds.at(0),
            nothing: false
        } : {
            nothing: true
        };

    const strReport = (mbd.nothing ? (() => {
            const nd = new tp3.Document();
            return (
                ds.push(nd), // Effect
                nd // Value
            );
        })() : mbd.just)
        .evaluate({
            script: taskpaperContext.toString(),
            withOptions: dctOptions
        });

    // EFFECT – REPORT MAYBE WRITTEN TO FILE ---------------------------------
    const mbWritten = isNull(strip(strReport)) ? {
        nothing: true
    } : writeFileMay(
        dctOptions.outPutFile,
        strReport
    );

    // VALUE - MESSAGE RETURNED ----------------------------------------------
    return mbWritten.nothing ? (
        'Nothing written to ' + standardPath(dctOptions.outPutFile)
    ) : (
        'Written to ' + standardPath(dctOptions.outPutFile) + '\n\n' +
        strReport
    );
})({
    outPutFile: '~/Desktop/report.taskpaper',
    itemsInPath: '/@type=project except Archive',
    orderBy: [
        '@priority descending as number',
        '@effort descending as number',
    ],
    groupBy: '@priority as number descending'
});


#23

Thank you very much. I just have a question. Is there a particular reason why all the @priority tags are changed into @t in the report?

Now, is it possible to simplify this script even a little bit more and make this into something that just runs a query and makes a new file with the results. In that way I can first understand what is going on and how you can create new files. Then I can go on to try to figure out how the sorting and ordering is taking place. There is no rush. so just whenever you get a chance.

Again, Thank you very much. Have. good weekend.


#24

Is there a particular reason why all the @priority tags are changed into @t in the report?

Thanks – fixed above in 0.02

Simpler version

Conceptually, there are, as you know, two bits of code in all these scripts:

  1. A single function for TaskPaper (possibly enclosing other functions inside it), which JXA converts into a string, and submits to the other Javascript interpreter, embedded inside TaskPaper
  2. The JXA code.

The key point here is that the TaskPaper code evaluation can return a value to JXA. e.g.

const strReport = doc.evaluate({
            script: taskpaperContext.toString(),
            withOptions: dctOptions
        });

Once JXA has received that return value, it can write it out to a file. For this stage, you need a function like:

// writeFile :: FilePath -> String -> IO ()
const writeFile = (strPath, strText) =>
    $.NSString.alloc.initWithUTF8String(strText)
    .writeToFileAtomicallyEncodingError(
        $(strPath)
        .stringByStandardizingPath, false,
        $.NSUTF8StringEncoding, null
    );

or, if you are feeling more cautious, or interested to trap failure conditions:

    // writeFileMay :: FilePath -> String -> Maybe FilePath
    const writeFileMay = (strPath, strText) => {
        const
            e = $(),
            fullPath = $(strPath)
            .stringByStandardizingPath;
        return $.NSString.alloc.initWithUTF8String(strText)
            .writeToFileAtomicallyEncodingError(
                fullPath, false,
                $.NSUTF8StringEncoding, e
            ) ? {
                nothing: false,
                just: ObjC.unwrap(fullPath)
            } : {
                nothing: true
            };
    };

Have to go out now, but I’ll aim to sketch you a minimal sorter for this pattern later.


#25

Vanilla and option-free:

(() => {
    'use strict';

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = editor => {

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        return editor.outline.evaluateItemPath(
                '/@type=project except Archive'
            )
            .sort(comparing(x => x.getAttribute('data-priority', Number)))
            .map(x => x.bodyString)
            .join('\n');
    };

    // JXA CONTEXT------------------------------------------------------------

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    const
        ds = Application('TaskPaper')
        .documents;

    const strReturnValue = ds.length > 0 ? ds.at(0)
        .evaluate({
            script: taskpaperContext.toString()
        }) : 'No document open in TaskPaper 3';

    return (
        writeFile('~/Desktop/test.taskpaper', strReturnValue),
        strReturnValue
    );
})();


#26

This is amazing. Now I am going to be googling how to run Ruby Scripts from JavaScript. Really. This works perfectly. I took your code and simplified a little bit more. I eliminated the “sort” option since part of what I wanted to see how to do was to run a query and then write the results into a new file. Then I could start to process the rest.

Here is your code minus the sort part.

(() => {
    'use strict';

    // ------------------------- TASKPAPER 3 CONTEXT --------------------------
    const taskpaperContext = editor => {

        return editor.outline.evaluateItemPath(
                '(@type/..* union task///*) except (( //@private/..* union @private///*) union (archive///* union @done))'
            )
            .map(x => x.bodyString)
			.join('\n');
    };

    // ---------------------------- JXA CONTEXT ------------------------------

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    const
        ds = Application('TaskPaper')
        .documents;

    const strReturnValue = ds.length > 0 ? ds.at(0)
        .evaluate({
            script: taskpaperContext.toString()
        }) : 'No document open in TaskPaper 3';

    return (
        writeFile('~/Desktop/test.taskpaper', strReturnValue),
        strReturnValue
    );
})();

In my particular case, I don’t need to see the position of the projects in the document tree (represented with indentations in TaskPaper), but it might be good for the code to keep that. Is that something easy to solve?


#27

I think that item.getattribute will let you read the value of the built-in attribute indent (which doesn’t have a ‘data-’ prefix)

Or, perhaps more directly, you can use the item.depth property


#28

Thank you. It is hard to figure out where to find documentation to understand the code. I think that I am starting to put the puzzle together.

Now, this is a question to @jessegrosjean

.map(x => x.bodyString)

Why is the indentation not included in the bodyString method? Is there a way to do this easily? Can you extend the code so that one could just get some of the information (what type of item this is, the text, and the tabs, but ignore the tags, etc.)?


#29

Why is the indentation not included in the bodyString method? Is there a way to do this easily?

The API is very complete – all of the information is there and flexibly usable.

If you wanted to preserve indentation, for example, one way would be to add tabs or spaces * (Item.depth - 1):

map(x => concat(replicate(x.depth - 1, '\t')) + x.bodyString)

As in

(() => {
    'use strict';

    // Vanilla 0.2 (with indents)

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = editor => {

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // concat :: [[a]] -> [a]
        // concat :: [String] -> String
        const concat = xs =>
            xs.length > 0 ? (() => {
                const unit = typeof xs[0] === 'string' ? '' : [];
                return unit.concat.apply(unit, xs);
            })() : [];

        // replicate :: Int -> a -> [a]
        const replicate = (n, x) =>
            Array.from({
                length: n
            }, () => x);

        return editor.outline.evaluateItemPath(
                '//@type=project except Archive'
            )
            .sort(comparing(x => x.getAttribute('data-priority', Number)))
            .map(x => concat(replicate(x.depth - 1, '\t')) + x.bodyString)
            .join('\n');
    };

    // JXA CONTEXT------------------------------------------------------------

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    const
        ds = Application('TaskPaper')
        .documents;

    const strReturnValue = ds.length > 0 ? ds.at(0)
        .evaluate({
            script: taskpaperContext.toString()
        }) : 'No document open in TaskPaper 3';

    return (
        writeFile('~/Desktop/test.taskpaper', strReturnValue),
        strReturnValue
    );
})();


#30

Why is the indentation not included in the bodyString method?

I can’t speak for Jesse, but as a scripter I can tell you that it would often prove a nuisance if it were included as a literal string rather than as a separate indent/depth number :- )

(When we didn’t need the leading whitespace we would need to manually remove it ourselves, and when we did need to know its length we would need to manually measure it ourselves.

The API gives us the line pre-analysed, with a number for the indent, and a string for the text)


#31

Thank you for all the help and patience. I am not faulting the documentation. I realize that my ignorance of JavaScript is the biggest hurdle in this process. I still get frustrated because I don’t know common functions that make something complicated easy. For example, I was looking at your concat method (is that what they are called in JavaScript?) and I couldn’t figure out where was xs coming from. I couldn’t even figure out why concat was even necessary.

Let me walk you through another one of my questions.

I wanted to see if one could do something like sanitize the items in order to eliminate all tags and their values. So I went through the documentation. I figured that one would have to go through each of the items and see its attributes, and then eliminate them. So… I know that;

  1. One would have to use the forEach(function (each) {} method.
  2. One would have to use the attributes method from the Birch library on each of the items.
  3. One would have to use something that goes through ever attribute (no idea what method to use to accomplish this, maybe go through another forEach).
  4. Delete each attribute using the removeAttribute() method found in Birch.

OR

  1. Use the bodyContentString and then add the additional text that makes that a task or a project if that is what they are.
  2. How does one find that additional text that makes an item a project or a task, I couldn’t figure that out from the available documentation.
  3. How does one add that in the correct place (The colon at the end to make it a project, or the dash at the beginning to make it a task), again, no clue.

So… ¯_(ツ)_/¯

You see. There is my dilemma. I know enough to get frustrated. The documentation gives me clues, but doesn’t show me how to do it with my limited knowledge


#32

sanitize the items in order to eliminate all tags and their values.

A function from a TP3 Item to a tagless TaskPaper string might look something like this:

// taglessTP3 :: Item -> String
const taglessTP3 = item => {
    const
        s = item.bodyContentString,
        type = item.getAttribute('data-type');
    return type !== 'task' ? (
        type !== 'project' ? (
            s // note
        ) : s + ':' // project
    ) : '- ' + s; // task
};

and could be used like this:

(() => {
    'use strict';

    // Vanilla 0.2 (with indents)

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = editor => {

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // concat :: [[a]] -> [a]
        // concat :: [String] -> String
        const concat = xs =>
            xs.length > 0 ? (() => {
                const unit = typeof xs[0] === 'string' ? '' : [];
                return unit.concat.apply(unit, xs);
            })() : [];

        // replicate :: Int -> a -> [a]
        const replicate = (n, x) =>
            Array.from({
                length: n
            }, () => x);

        // taglessTP3 :: Item -> String
        const taglessTP3 = item => {
            const
                s = item.bodyContentString,
                type = item.getAttribute('data-type');
            return type !== 'task' ? (
                type !== 'project' ? (
                    s // note
                ) : s + ':' // project
            ) : '- ' + s; // task
        };

        return editor.outline.evaluateItemPath(
                // '//@type=project except Archive'
                '//*'
            )
            .sort(comparing(x => x.getAttribute('data-priority', Number)))
            .map(x => concat(replicate(x.depth - 1, '\t')) + taglessTP3(x))
            .join('\n');
    };

    // JXA CONTEXT------------------------------------------------------------

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    const
        ds = Application('TaskPaper')
        .documents;

    const strReturnValue = ds.length > 0 ? ds.at(0)
        .evaluate({
            script: taskpaperContext.toString()
        }) : 'No document open in TaskPaper 3';

    return (
        writeFile('~/Desktop/test.taskpaper', strReturnValue),
        strReturnValue
    );
})();

#33

I quite understand.

your concat method

In the two comment lines above the generic concat function (a ‘method’ would probably be a function attached to an object),

// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs =>
    xs.length > 0 ? (() => {
        const unit = typeof xs[0] === 'string' ? '' : [];
        return unit.concat.apply(unit, xs);
    })() : [];

… an a means an input (or output) value of any kind (string, number, bool etc), and [a] means a list/array of any kind of value. By extension [[a]] is a list of lists of any kind of value.

concat can either be a function from [[a]] (a list of lists of anything) to [a] (a single list of same kind of value):

// concat :: [[a]] -> [a]
concat([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]) 

// -> [1,2,3,4,5,6,7,8,9]

or, a function from a [String] (list of strings) to String (a single string):

// concat :: [String] -> String
concat(['alpha', 'beta', 'gamma'])

// -> "alphabetagamma"

We need it there because the type of the replicate function is:

replicate :: Int -> a -> [a]

Which means that given an integer and a value of any kind, it returns a list of that kind of value.

replicate(3, 'alpha')

// -> ["alpha","alpha","alpha"]

With the addition of a concat stage to the pipeline, this becomes a single concatenated string:

concat(replicate(3, 'alpha'))

// -> "alphaalphaalpha"

Finally, xs is the name bound to the input value (argument) supplied to concat, which could also be written in the alternative format:

// concat :: [[a]] -> [a]
// concat :: [String] -> String
function concat(xs) {
    return xs.length > 0 ? function () {
        var unit = typeof xs[0] === 'string' ? '' : [];
        return unit.concat.apply(unit, xs);
    }() : [];
};

#34

I personally find it fastest to build scripts like Lego constructions by pasting generic primitives like replicate and concat from a menu of about 250 such functions.

(I also keep a parallel set in AppleScript for comparison, and use fairly well-worn generic function names from the ML and Haskell tradition - so it gives me a portable style of composing things quite quickly)

Too big to post in a thread, but message me here if you think you might like a copy. (The pasting interface depends on having a copy of Quiver, but you can also just hunt and copy through a text file – the Script Editor / Apple Automation Library mechanism, is, alas not up to it : -)