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 - #4 by mylevelbest)
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();
})();