The standard JS Array.map applies some function to every item in a list, one by one, returning a new, transformed Array - the order and structure of which is preserved.
[1, 2, 3, 4, 5].map(x => x * 2)
// -> [2, 4, 6, 8, 10]
Here is a more generic map, which applies some function to every item in a TP outline, returning transformed/derived contents in a preserved outline structure.
// fmapTPTree :: (TPItem -> a) -> TPItem -> Tree a
const fmapTPTree = (f, tpItem) => {
const go = x => Node(f(x),
x.hasChildren ? (
x.children.map(go)
) : []);
return go(tpItem);
};
If we give it a function (from a TP Item to some value), and a particular TP Item, it applies the given function both to that Item, and to all of its descendants, returning a generic Tree structure, which has the same shape (same pattern of parents and children) as the TP Outline.
(Every item in the Tree is a Node, which has a root (some kind of value like a string or number or JS dictionary – created by the function which we supplied), and a nest, which is a list of child Nodes.
I personally use this generic Tree pattern as a hub format, for conversions between outlines and nested diagrams etc.
To get from a TaskPaper outline to OPML, for example, we can:
- Use fmapTPTree to translate from a TaskPaper outline to the hub Tree format, and then
- A generic Tree -> OPML function for the second stage.
(Using a generic hub format reduces the number of translation pairs that we need to write)
Stage one (in the TaskPaper JSContext)
(Where itemTextAndTags is a function returning the details which we want for any given item)
dctTree = fmapTPTree(
itemTextAndTags,
editor.outline.root
)
Stage two (in the Javascript for Automation JSContext)
opmlFromTrees('Outline title', dctTree.nest)
Here is a fuller sketch, which copies the open TP Outline as OPML:
(() => {
'use strict';
// GENERIC TREE FROM TASKPAPER -------------------------------------------
const tpJSContext = (editor, options) => {
// GENERIC FUNCTIONS FOR TP CONTEXT ----------------------------------
// Node :: a -> [Tree a] -> Tree a
const Node = (v, xs) => ({
type: 'Node',
root: v, // any type of value (but consistent across tree)
nest: xs
});
// fmapTPTree :: (TPItem -> a) -> TPItem -> Tree a
const fmapTPTree = (f, tpItem) => {
const go = x => Node(f(x),
x.hasChildren ? (
x.children.map(go)
) : []);
return go(tpItem);
};
// showJSON :: a -> String
const showJSON = x => JSON.stringify(x, null, 2);
// TP CONTEXT MAIN ---------------------------------------------------
// itemTextAndTags :: TPItem ->
// { text :: String, kvs :: [(String, String))] }
const itemTextAndTags = tpItem => {
const attribs = tpItem.attributes;
return {
text: tpItem.bodyContentString,
kvs: tpItem.attributeNames.reduce(
(a, k) => [ // Tags and values except 'type' + 'indent'.
'indent', 'data-type'
].includes(k) ? (
a // Unmodified accumulator
) : a.concat([
[ // Key-value pair added to accumulator
k.startsWith('data-') ? (
k.slice(5) // 'data-' prefix dropped
) : k, attribs[k]
]
]), []
)
}
};
return showJSON(
fmapTPTree(
itemTextAndTags,
editor.outline.root
)
);
};
// GENERIC FUNCTIONS FOR JXA CONTEXT -------------------------------------
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b
});
// concat :: [[a]] -> [a]
// concat :: [String] -> String
const concat = xs =>
xs.length > 0 ? (() => {
const unit = typeof xs[0] === 'string' ? '' : [];
return unit.concat.apply(unit, xs);
})() : [];
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x, ...xs];
// intercalate :: [a] -> [[a]] -> [a]
// intercalate :: String -> [String] -> String
const intercalate = (sep, xs) =>
concat(intersperse(sep, xs));
// intersperse(0, [1,2,3]) -> [1, 0, 2, 0, 3]
// intersperse :: Char -> String -> String
// intersperse :: a -> [a] -> [a]
const intersperse = (sep, xs) => {
const bool = (typeof xs)[0] === 's';
return xs.length > 1 ? (
(bool ? concat : x => x)(
(bool ? (
xs.split('')
) : xs)
.slice(1)
.reduce((a, x) => a.concat([sep, x]), [xs[0]])
)) : xs;
};
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// 'The mapAccumL function behaves like a combination of map and foldl;
// it applies a function to each element of a list, passing an accumulating
// parameter from left to right, and returning a final value of this
// accumulator together with the new list.' (See Hoogle)
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
const mapAccumL = (f, acc, xs) =>
xs.reduce((a, x, i) => {
const pair = f(a[0], x, i);
return Tuple(pair[0], a[1].concat(pair[1]));
}, Tuple(acc, []));
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// unwords :: [String] -> String
const unwords = xs => xs.join(' ');
// words :: String -> [String]
const words = s => s.split(/\s+/);
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = (f, xs, ys) =>
Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => f(xs[i], ys[i], i));
// OPML FROM GENERIC TREE ------------------------------------------------
// opmlFromTrees :: String -> [Tree] -> OPML String
const opmlFromTrees = (strTitle, xs) => {
const
// ents :: [(Regex, String)]
ents = zipWith.apply(null,
cons(
(x, y) => [new RegExp(x, 'g'), '&' + y + ';'],
map(words, ['& \' " < >', 'amp apos quot lt gt'])
)
),
// entCoded :: a -> String
entCoded = v => ents.reduce(
(a, [x, y]) => a.replace(x, y),
v.toString()
),
// Nest -> Comma-delimited row indices of all parents in tree
// expands :: [textNest] -> String
expands = xs => {
const indexAndMax = (n, xs) =>
mapAccumL((m, node) =>
node.nest.length > 0 ? (() => {
const sub = indexAndMax(m + 1, node.nest);
return [sub[0], cons(m, concat(sub[1]))];
})() : [m + 1, []], n, xs);
return intercalate(
',',
indexAndMax(0, xs)[1].map(x => x.toString())
);
};
// nodeOPML :: String -> Node -> String
const nodeOPML = indent => x => {
const
root = x.root,
nest = x.nest;
return indent + '<outline ' + unwords(map(
([k, v]) => k + '="' + entCoded(v) + '"',
cons(['text', root.text], root.kvs)
)) + (nest.length > 0 ? (
'>\n' +
unlines(map(nodeOPML(indent + ' '), nest)) +
'\n' +
indent + '</outline>'
) : '/>');
};
// OPML serialization --------------------------------------------
return unlines(concat([
[
'<?xml version=\"1.0\" encoding=\"utf-8\"?>',
'<opml version=\"2.0\">',
' <head>',
' <title>' + strTitle + '</title>',
' <expansionState>' + expands(xs) + '</expansionState>',
' </head>',
' <body>'
],
map(nodeOPML(' '), xs), [
' </body>',
'</opml>'
]
]));
};
// JXA 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
});
// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
m.Right !== undefined ? (
mf(m.Right)
) : m;
// standardAdditions :: () -> Application
const standardAdditions = () =>
Object.assign(Application.currentApplication(), {
includeStandardAdditions: true
});
const
ds = Application('TaskPaper')
.documents,
lrResult = bindLR(
bindLR(
bindLR(
ds.length > 0 ? Right(ds.at(0)) : Left(
'No documents open'
),
d => Right(d.evaluate({
script: tpJSContext.toString(),
withOptions: {}
}))
),
strJSON => {
let dctTree = {};
try {
dctTree = JSON.parse(strJSON);
} catch (e) {
return Left(e.message)
}
return Right(
opmlFromTrees('Outline title', dctTree.nest)
);
}
),
strOPML => (
standardAdditions()
.setTheClipboardTo(strOPML),
Right(strOPML)
)
);
return lrResult.Right || lrResult.Left;
})();