Script :: Create and copy a bookmark to the selected item

Creates, applies, and copies a filter path to the item selected in TaskPaper 3.

Can be used in conjunction with Hook to create clickable links to particular points in TaskPaper files. (See also Using Hook with TaskPaper3)

JavaScript 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();
})();

3 Likes

And one obvious variant of this basic script would be to append

/descendant-or-self::*

to the bookmark, so that the bookmarked item is displayed with any descendants which it may have.

So, for example:

(() => {
    'use strict';

    // RobTrew 2019
    // Ver 0.03

    // Added affix option (e.g. appending '/descendant-or-self::*')

    // 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(),
                    withOptions: {
                        affix: '/descendant-or-self::*'
                    }
                })
            ) : '';
        return 0 < bookMarkPath.length ? (
            copyText(bookMarkPath),
            bookMarkPath
        ) : '';
    };

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

        // TP3 MAIN
        const main = () => {
            const
                seln = editor.selection,
                iEnd = seln.endOffset,
                iStart = seln.startOffset,
                strPath = itemWordPath(seln.startItem) + (
                    options.affix || ''
                ),
                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();
})();
2 Likes