Gathering existing TaskPaper 3 searches for use in a new document

A TaskPaper 3 plain text outline can contain a number of custom perspectives or 'saved searches'

Here is a macro for gathering existing searches into a menu, so that you can add some subset of them to a new TaskPaper 3 document. The macro gathers searches into the menu from:

  1. All open TaskPaper 3 documents, and
  2. any default TaskPaper 3 documents that are listed in the macro (whether they are open or not)

Add existing TaskPaper 3 searches to new document.kmmacros (28.5 KB)

Source of the JavaScript for Automation action:

// SEARCH MENU WHICH DRAWS ITS ITEMS FROM ALL OPEN TASKPAPER 3 DOCS, 
// AND OPTIONALLY ALSO FROM DEFAULT TASKPAPER 3 DOCS, WHICH CAN BE CLOSED
//
//      Offers and adds saved searches
//
//      FROM: 1. All open TaskPaper 3 documents
//            2. One or more default TaskPaper documents 
//               (give path(s) in options at bottom of script)
//               which don't need to be open.
//
//      TO:   The active (front) TaskPaper 3 document

// NB
// Adjust the options at the bottom of the script before use
// 
// Rob Trew ver .015


(function (dctOptions) {
    'use strict';

    // TASKPAPER CONTEXT

    // Editor -> [String]
    function fnFindSearches(e) {
        return e.outline.evaluateItemPath('//@search')
            .map(function (x) {
                return x.bodyString;
            });
    }

    // Editor -> Options -> () -> String
    function fnUseSearches(editor, options) {

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

        // searchLine :: String -> String
        function searchLine(s) {
            var lstParts = s.split(rgxDelim);

            return lstParts[0] + ' @search(' + lstParts[1]
                .replace(rgxRBrkt, '\\)') + ')';
        }

        var rgxDelim = /\s*\t/,
            rgxRBrkt = /\)/g,
            outline = editor.outline,

            // DIFFERENCE BETWEEN LIST OF SEARCHES 
            // ALREADY IN THE DOCUMENT
            lstChosen = options.searches,
            strInDoc = outline.evaluateItemPath('//@search')
            .map(function (x) {
                return x.bodyString;
            })
            .join('\n'),

            // AND LIST OF SEARCHES CHOSEN IN DIALOG
            lstSearches = strInDoc.length > 0 ? (
                concatMap(function (x) {
                    var strSearch = searchLine(x);

                    return strInDoc.indexOf(strSearch) === -1 ? (
                    [strSearch]
                    ) : [];
                }, lstChosen)) : lstChosen.map(searchLine);

        // MAYBE ADD NEW SEARCHES TO THE ACTIVE DOCUMENT
        if (options.addSearches && lstSearches.length > 0) {
            var root = outline.root;

            outline.groupUndoAndChanges(function (x) {
                root.insertChildrenBefore(
                    ItemSerializer.deserializeItems(
                        lstSearches.join('\n'),
                        outline,
                        ItemSerializer.TEXTMimeType
                    ),
                    root.firstChild
                );
            });
        }

        // MAYBE APPLY A SEARCH TO THE ACTIVE DOCUMENT
        if (options.applySearch && (lstChosen.length === 1)) {
            editor.itemPathFilter = lstChosen[0].split(rgxDelim)[1];
        }
    }

    // JAVASCRIPT FOR AUTOMATION CONTEXT

    // Reduce a list to a subset of its elements
    // that is unique in terms of the equality defined by
    // a supplied function - fnEq

    // 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);
                    }
                )
            )
        ) : [];
    }

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

    // fileExists :: String -> Bool
    function fileExists(strPath) {
        var fm = $.NSFileManager.defaultManager,
            error = $();

        fm.attributesOfItemAtPathError(
            ObjC.unwrap($(strPath)
                .stringByExpandingTildeInPath),
            error
        );

        return error.code === undefined;
    }

    // String -> String
    function textFileContents(strPath) {
        return $.NSString.stringWithContentsOfFile(strPath);
    }

    // Array of any default filepath(s) given/listed in options at end
    var varDefault = dctOptions.defaultFiles,
        lstPaths = varDefault ? (
            concatMap(function (x) {
                var s = x.trim();
                return s.length > 0 ? [s] : [];
            }, (varDefault instanceof Array ? varDefault : [varDefault]))
        ) : [];

    // COMBINE ALL SEARCHES IN THE DEFAULT FILE(S)
    var tp3 = Application("TaskPaper"),
        ds = tp3.documents(),
        strText = lstPaths.reduce(function (a, strPath) {
            return a + ((fileExists(strPath)) ? textFileContents($(
                        strPath)
                    .stringByExpandingTildeInPath)
                .js : '');
        }, ''),
        lstSearches = strText.split(/[\n\r]+/)
        .filter(function (s) {
            return s.indexOf('@search') !== -1;
        })

    // WITH ALL SEARCHES IN ALL OPEN TP3 DOCS
    .concat(concatMap(function (d) {
        return d.evaluate({
            script: fnFindSearches.toString(),
        })
    }, ds))

    // EXTRACTING SEARCH NAMES AND PATHS
    .map(function (x) {
            var lstParts = x.split('@search('),
                strPath = lstParts[1];
            return {
                name: lstParts[0].split(/\W/)
                    .filter(function (str) {
                        return str.length > 0;
                    })
                    .join(' '),

                path: strPath.substr(0, strPath.lastIndexOf(')'))
                    .replace(/\\\)/g, ')')
            }
        }),

        // REDUCING THEM DOWN TO A UNIQUE LIST OF PATHS
        lstMenu = nubBy(function (a, b) {
            return a.path === b.path;
        }, lstSearches)
        .sort(function (a, b) {
            var strA = a.name.toLowerCase(),
                strB = b.name.toLowerCase();

            return strA < strB ? -1 : (strA > strB ? 1 : 0);
        });

    // OFFERING THEM AS A MENU
    if (lstMenu.length > 0) {
        var su = Application("SystemUIServer"),
            sa = (su.includeStandardAdditions = true, su),

            // DISPLAYING THE APPLY AND/OR ADD OPTIONS
            strApply = dctOptions.applySearch ? 'apply' : '',
            strAdd = dctOptions.addSearches ? 'add' : '',
            strAnd = (strApply.length && strAdd.length) ? ' and ' : '',
            strVerbs = strApply + strAnd + strAdd,

            // AIMING TO FORMAT NAME AND PATH IN
            // DISTINCT COLUMNS
            lngChars = lstMenu.reduce(function (a, b) {
                var lng = b.name.length;

                return lng > a ? lng : a
            }, 0),
            lst = lstMenu.map(function (x) {
                var strName = x.name,
                    strPad = Array(lngChars + 1)
                    .join(' ');

                return (strName + strPad)
                    .slice(0, 12) + '\t' + x.path;
            });

        // AND GETTING THE USER'S CHOICE
        sa.activate();
        var varChoice = sa.chooseFromList(lst, {
                withTitle: "TaskPaper 3 saved searches",
                withPrompt: 'Gathered from all open docs' + (
                    lstPaths.length > 0 ? ',\nand from:\n' +
                    lstPaths.map(
                        function (strPath) {
                            return '\n\t' + strPath;
                        }) : ''
                ) + '\n\nChoose search(es) to add:',
                defaultItems: lst[0],
                okButtonName: 'OK',
                cancelButtonName: 'Cancel',
                multipleSelectionsAllowed: dctOptions.addSearches,
                emptySelectionAllowed: true
            }),
            lstChoice = varChoice ? varChoice : [];

        // IF A SEARCH HAS BEEN CHOSEN ...
        if (lstChoice.length > 0) {

            tp3.activate();

            // APPLY AND/OR ADD THE SEARCH 
            // (IT IS NOT ALREADY IN THE DOC)
            // SEE OPTIONS
            return ds.length ? ds[0].evaluate({
                script: fnUseSearches.toString(),
                withOptions: {
                    searches: lstChoice,
                    applySearch: dctOptions.applySearch,
                    addSearches: dctOptions.addSearches
                }
            }) : undefined;
        }
    }


})({
    // Bool
    applySearch: false, // Filter active doc with chosen search (if single)

    // Bool
    addSearches: true, // Add chosen search(es) if not in active doc

    // String | [String] | undefined
    defaultFiles: Application("Keyboard Maestro Engine")
        .variables['tp3MainFilePaths'].value()
        .split(/[\n\r]+/)
});
1 Like