In the meanwhile, here is a very basic (few options) script which aims to copy the active TaskPaper 3 document to the clipboard as Markdown.
For the moment it assumes that you want your top-level headings to have a single MD # hash, and that all projects are hash headings.
(Could add an option to change that if anyone else does use this script)
(You can adjust whether or not to include the TaskPaper tags by editing the options
dictionary at the top of the script)
const options = {
includeTags: true,
// If wholeDoc is *false*, only items currently
// displayed in the front document are copied as MD.
wholeDoc: true
};
It’s a JavaScript for Automation script which you should be able to run either from Script Editor, with the language selector at top left set to JavaScript, or by assigning it to a keyboard shortcut with something like Keyboard Maestro or FastScripts. See Using Scripts in the TaskPaper User’s Guide.
(Be sure to copy every line of the code below. The last lines are:
// MAIN ---
return main();
})();
JavaScript source – click disclosure triangle
(() => {
'use strict';
ObjC.import('AppKit');
// Copy TaskPaper 3 active document to clipboard
// as basic Markdown.
// Rob Trew 2020
// Ver 0.04 wholeDoc=false -> folded lines hidden.
// Ver 0.03
// - Added a wholeDoc option. If this is set to false
// only focused projects will be copied as Markdown.
// Ver 0.02
// - Extended to cover top level lines which
// are not projects.
// - Simplified TP Context code - projects list
// harvested with evaluateItemPath.
const options = {
includeTags: true,
// If wholeDoc is false, only items currently
// displayed in the front document are copied as MD.
wholeDoc: true
};
// ---------------------JXA CONTEXT---------------------
const main = () => {
const ds = Application('TaskPaper').documents;
return either(
alert('Problem')
)(
compose(
alert('MD copied to clipboard'),
copyText,
mdFromTPForest
)
)(
bindLR(
0 < ds.length ? (
Right(ds.at(0))
) : Left('No TaskPaper documents open')
)(
d => d.evaluate({
script: tp3ProjectForest.toString(),
withOptions: options
})
)
);
};
// ------------------TASKPAPER CONTEXT------------------
// tp3ProjectForest :: TP Editor -> [Tree Dict]
const tp3ProjectForest = (editor, options) => {
const showAll = options.wholeDoc;
const main = () => Right(
// Leftmost lines, or projects at any level.
// (in whole document, or in visible focus)
(
showAll ? (
editor.outline.evaluateItemPath(
'/* union //project'
)
) : visibleLeftmostAndProjects()
)
// with any subtree of each,
// (excluding sub-projects)
.map(topLevel => foldTree(
item => nest =>
Node(item)(
nest.filter(
x => {
const dct = x.root;
return (
showAll || dct.visible
) && ('project' !== dct.kvs[
'data-type'
])
}
)
)
)(fmapPureTPTree(x => ({
text: x.bodyContentString,
kvs: x.attributes,
depth: x.depth,
visible: editor.isDisplayed(x),
projectLevel: 'project' !== topLevel
.getAttribute('data-type') ? (
0
) : topLevel.depth
}))(topLevel)))
);
// visibleLeftmostAndProjects :: () -> [TP Item]
const visibleLeftmostAndProjects = () => {
const
xs = editor.displayedItems,
outermost = minimum(xs.map(x => x.depth));
return xs.filter(
x => outermost === x.depth || (
'project' === x.attributes['data-type']
)
);
};
// --------GENERIC FUNCTIONS FOR TP CONTEXT---------
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v => xs => ({
type: 'Node',
root: v,
nest: xs || []
});
// fmapPureTPTree :: (TPItem -> a) -> TPItem -> Tree TPItem
const fmapPureTPTree = f => item => {
const go = x => Node(f(x))(
x.hasChildren ? (
x.children.map(go)
) : []
);
return go(item);
};
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f =>
// The catamorphism on trees. A summary
// value obtained by a depth-first fold.
tree => {
const go = x => f(x.root)(
x.nest.map(go)
);
return go(tree);
};
// minimum :: Ord a => [a] -> a
const minimum = xs =>
0 < xs.length ? (
xs.slice(1)
.reduce((a, x) => x < a ? x : a, xs[0])
) : undefined;
return main();
};
// FUNCTIONS FOR JXA
// --------------BASIC MARKDOWN FUNCTIONS---------------
// mdFromTPForest :: [Tree Dict] -> String
const mdFromTPForest = xs => {
const blnTags = options.includeTags;
return concat(xs.map(foldMapTree(x =>
' '.repeat(
x.depth - (
x.projectLevel + (
isProject(x) ? 0 : 1
)
)
) + mdPrefix(x) + x.text + (
blnTags ? mdTags(x) : ''
) + '\n'
)));
};
// isProject :: Dict -> Bool
const isProject = x =>
'project' === x.kvs['data-type'];
// mdPrefix :: Dict -> String
const mdPrefix = x => {
const type = x.kvs['data-type'];
return 'note' !== type ? (
'task' !== type ? (
'project' !== type ? (
''
) : ('\n' + '#'.repeat(x.depth) + ' ')
) : '- '
) : '';
};
// mdTags :: Dict -> String
const mdTags = x => {
const
dct = x.kvs,
ks = Object.keys(dct).filter(
k => k.startsWith(
'data'
) && k !== 'data-type'
);
return 0 < ks.length ? (
' @' + ks.map(
k => 0 < dct[k].length ? (
`${k.slice(5)}(${dct[k]})`
) : k.slice(5)
).join(' @')
) : '';
};
// ------------------JXA FUNCTIONS------------------
// alert :: String => String -> IO String
const alert = title => s => {
const
sa = Object.assign(
Application('System Events'), {
includeStandardAdditions: true
});
return (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ['OK'],
defaultButton: 'OK'
}),
s
);
};
// copyText :: String -> IO String
const copyText = s => {
// String copied to general pasteboard.
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// GENERIC FUNCTIONS ----------------------------
// https://github.com/RobTrew/prelude-jxa
// Just :: a -> Maybe a
const Just = x => ({
type: 'Maybe',
Nothing: false,
Just: x
});
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = v =>
// Constructor for a Tree node which connects a
// value of some kind to a list of zero or
// more child trees.
xs => ({
type: 'Node',
root: v,
nest: xs || []
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// append (++) :: [a] -> [a] -> [a]
// append (++) :: String -> String -> String
const mappend = xs =>
// A list or string composed by
// the concatenation of two others.
ys => xs.concat(ys);
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => undefined !== m.Left ? (
m
) : mf(m.Right);
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
x => fs.reduceRight((a, f) => f(a), x);
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs =>
0 < xs.length ? (
xs.every(x => 'string' === typeof x) ? (
''
) : []
).concat(...xs) : xs;
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
fr => e => 'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// foldMapTree :: Monoid m => (a -> m) -> Tree a -> m
const foldMapTree = f =>
// Result of mapping each element of the tree to
// a monoid, and combining with mappend.
node => {
const go = x =>
0 < x.nest.length ? mappend(f(x.root))(
foldl1(mappend)(x.nest.map(go))
) : f(x.root);
return go(node);
};
// foldl1 :: (a -> a -> a) -> [a] -> a
const foldl1 = f =>
// Left to right reduction of the non-empty list xs,
// using the binary operator f, with the head of xs
// as the initial acccumulator value.
xs => 1 < xs.length ? xs.slice(1)
.reduce(uncurry(f), xs[0]) : xs[0];
// uncurry :: (a -> b -> c) -> ((a, b) -> c)
const uncurry = f =>
// A function over a pair, derived
// from a curried function.
(x, y) => f(x)(y);
// MAIN ---
return main();
})();