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