Copy a Bookmark (filter path) to the selected TaskPaper 3 item Macro

Copy a Bookmark (filter path) to the selected TaskPaper 3 item Macro

Creates, applies and copies a TaskPaper3 itemPath filter which narrows the editor view to the selected item and its context.

Combined with Hook, this can be used to create clickable (markdown or other) links to specific points in TaskPaper 3 files.

(see https://discourse.hookproductivity.com/t/taskpaper-3-example-links-preserving-filter-state/564)

Copy a Bookmark (filter path) to the selected TaskPaper 3 item.kmmacros (26 KB)

JS Source

(() => {
    'use strict';

    // RobTrew 2019
    // Ver 0.01

    // TaskPaper :: create and copy a bookmark path to the selected item.

    // Creates, applies, and copies a short but legible item path filter
    // which focuses the editor display on just the currently selected item
    // in TaskPaper 3.

    // e.g. for use with [Hook.app](https://hookproductivity.com)

    // Hook scripts for filter-preserving TaskPaper urls at:
    // https://support.hogbaysoftware.com/t/using-hook-with-taskpaper3/4151

    // JS FOR AUTOMATION CONTEXT --------------------------
    const main = () => {
        const
            ds = Application('TaskPaper').documents,
            bookMarkPath = 0 < ds.length ? (
                ds.at(0).evaluate({
                    script: tp3Context.toString()
                })
            ) : '';
        return 0 < bookMarkPath.length ? (
            copyText(bookMarkPath),
            bookMarkPath
        ) : '';
    };

    // TASKPAPER CONTEXT ----------------------------------
    const tp3Context = editor => {

        // TP3 MAIN
        const main = () => {
            const
                seln = editor.selection,
                iEnd = seln.endOffset,
                iStart = seln.startOffset,
                strPath = itemWordPath(seln.startItem),
                matchedItems = editor.outline.evaluateItemPath(strPath);
            return (
                editor.itemPathFilter = strPath,
                0 < matchedItems.length ? (() => {
                    const match = matchedItems[0];
                    return (
                        // Selection restored in editor.
                        editor.moveSelectionToItems(
                            match, iEnd,
                            match, iStart
                        ),
                        strPath
                    );
                })() : strPath
            );
        };

        // Item path built from longest unique word
        // at each ancestral level.

        // itemWordPath :: TPItem -> String
        const itemWordPath = x => {
            const go = x => x.isOutlineRoot ? (
                ''
            ) : (() => {
                const
                    oParent = x.parent,
                    ks = longestUniquePeerWords(oParent.children),
                    reserved = [
                        'project', 'task', 'note',
                        'and', 'or', 'not'
                    ];
                return go(oParent) + '/' + (
                    0 < ks.length ? (
                        tokens(x.bodyContentString.toLocaleLowerCase())
                        .sort(descendingLength)
                        .reduce(
                            (a, w) => '*' !== a ? a : (
                                ks.includes(w) ? (
                                    reserved.includes(w) ? (
                                        'contains ' + w
                                    ) : w
                                ) : a
                            ),
                            '*'
                        )
                    ) : '*'
                );
            })();
            return go(x);
        };

        // longestUniquePeerWords :: [TP3 Item] -> [String]
        const longestUniquePeerWords = peerNodes =>
            group(
                peerNodes.flatMap(
                    x => tokens(
                        x.bodyContentString
                        .toLocaleLowerCase()
                    ).filter(w => 0 < w.length)
                ).sort(descendingLength)
            ) // Except multiply used words.
            .flatMap(xs => 1 < xs.length ? [] : xs[0])


        // TOKENIZATION -----------------------------------
        const
            strPunct = '[.,\'\"\(\)\[\\]\/#!$%\^&\*\-;:{}=\-_`~()]+',
            rgxStart = new RegExp('^' + strPunct, 'g'),
            rgxEnd = new RegExp(strPunct + '$', 'g');

        // tokens :: String -> [String]
        const tokens = s =>
            s.split(/[\s\/\’]+/).flatMap(
                w => {
                    const x = w.replace(rgxStart, '').replace(rgxEnd, '');
                    return 0 < x.length ? (
                        [x]
                    ) : [];
                }
            );

        // GENERICS FOR TASKPAPER CONTEXT -----------------

        // descendingLength :: String -> String -> Ordering
        const descendingLength = (y, x) => {
            const
                a = x.length,
                b = y.length;
            return a < b ? -1 : a > b ? 1 : 0
        };

        // group :: Eq a => [a] -> [[a]]
        const group = xs => groupBy((a, b) => a === b, xs);

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

        // TASKPAPER MAIN
        return main();
    };

    // JXA GENERIC ----------------------------------------

    // copyText :: String -> IO ()
    const copyText = s => {
        Object.assign(Application('System Events'), {
            includeStandardAdditions: true
        }).setTheClipboardTo(s);
    };

    // JS FOR AUTOMATION MAIN -----------------------------
    return main();
})();