Somebody asked me this week about an old script for TaskPaper 3 – Copy as Bike Outline
That earlier script still works, but here, FWIW, is a variant which:
- Maps Taskpaper
projectsto Bikeheadings - Maps
tasksandnotesdirectly from TaskPaper to Bike - Reads Taskpaper
@tag(value)to Biketag=value - Collects any bracketless
@tagpatterns to the space-delimited value of a singledata-classesattribute.
For example, this pattern in TaskPaper 3
To Organize Items:
- To indent items press the Tab key. @red @priority(2)
- To un-indent items press Shift-Tab.
- To mark a task done click leading dash.
Might look, in Bike 2.0, like:
And if you were to Save As Markdown text, those lines would become:
- # To Organize Items
- [ ] To indent items press the Tab key. {.red priority=2}
- [ ] To un-indent items press Shift-Tab.
- [ ] To mark a task done click leading dash.
Expand disclosure triangle to view JS source
(() => {
"use strict";
ObjC.import("AppKit");
// `Copy from TaskPaper 3 as Bike`
// ( Creats a com.hogbaysoftware.bike.xml pasteboard ).
// Copies selected lines of front TaskPaper 3 document
// (or all visible rows in the TP3 outline
// if the selection is collapsed).
// Rob Trew @2023, @2026
// Ver 0.02
// TP to Bike row type mappings:
// project -> heading
// task -> task
// note -> note
// TP to Bike tag mappings
// @tag(value) -> data-tag=value
// @tag -> .class
// Where .classes in Bike row are added to the space
// -delimited value of a single `data-classes` attribute.
// ------------- JXA EVALUATION CONTEXT --------------
// main :: IO ()
const main = () => {
const document = Application("TaskPaper").documents.at(0);
return either(
alert("Copy from Taskpaper 3 as Bike outline")
)(
bikeLines => (
setClipOfTextType(
"com.hogbaysoftware.bike.xml"
)(
`<?xml version="1.0" encoding="UTF-8"?>
<html>
<head><meta charset="utf-8" /></head>
<body><ul>${bikeLines}</ul></body>
</html>`
),
"TaskPaper copied as Bike outline."
)
)(
fmapLR(
doc => doc.evaluate({ script })
)(
document.exists()
? Right(document)
: Left("No document open in TaskPaper.")
)
);
};
// ---------- TASKPAPER EVALUATION CONTEXT -----------
const script = (editor => {
const tp3Main = () => {
const selection = editor.selection;
return (
selection.isCollapsed
? Item.getCommonAncestors(
editor.displayedItems
)
: selection.selectedItemsCommonAncestors
)
.flatMap(foldTree(bikeLine))
.join("\n");
};
// ------------------ BIKE XML -------------------
// bikeLine :: TP Node -> [String] -> [String]
const bikeLine = tpNode =>
xs => {
const
txt = tpNode.bodyContentString,
attribs = bikeAttributes(tpNode);
return [
// <li> opens,
0 < attribs.length
? [`<li ${attribs}>`]
: ["<li>"],
// <p>
[
0 < txt.length
? `<p>${txt}</p>`
: "<p/>"
],
// then <ul> if there are children,
0 < xs.length
? [`<ul>${xs.flat().join("\n")}</ul>`]
: [],
// and </li> closes.
["</li>"]
]
.flat();
};
// bikeAttributes :: TaskPaper Node -> String
const bikeAttributes = tpNode => {
const classesAndKVs = classes => k => {
const v = (dict[k] || "").trim();
return k.startsWith("data-")
? k === "data-type"
? [
classes, [
v === "project"
? 'data-type="heading"'
: `data-type="${v}"`
]
]
: 0 < v.length
? [classes, `${k}="${v}"`]
: [classes.concat(k.slice(5)), []]
: [classes, []];
return [[], [""]];
};
const
dict = tpNode.attributes,
[classes, kvs] = mapAccumL(classesAndKVs)([])(
Object.keys(dict)
);
return [
0 < kvs.length
? [kvs.flat().join(" ")]
: [],
0 < classes.length
? [`data-classes="${classes.join(' ')}"`]
: []
]
.flat()
.join(" ");
};
// ------- GENERICS FOR TASKAPAPER CONTEXT -------
// foldTree :: (a -> [b] -> b) -> Tree a -> b
const foldTree = f => {
// The catamorphism on trees. A summary
// value obtained by a depth-first fold.
const go = tree => f(
root(tree)
)(
nest(tree).map(go)
);
return go;
};
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
const mapAccumL = f =>
// A tuple of an accumulation and a list
// obtained by a combined map and fold,
// with accumulation from left to right.
acc => xs => {
const
n = xs.length,
ys = new Array(n);
let mutableAcc = acc;
for (let i = 0; i < n; i++) {
const [nextAcc, y] = f(mutableAcc)(xs[i], i);
mutableAcc = nextAcc;
ys[i] = y;
}
return [mutableAcc, ys];
};
// nest :: Tree a -> [a]
const nest = tree =>
tree.children;
// root :: Tree a -> a
const root = tree =>
// The value attached to a tree node.
tree;
return tp3Main();
})
.toString();
// ----------------------- JXA -----------------------
// 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
);
};
// setClipOfTextType :: String -> String -> IO String
const setClipOfTextType = utiOrBundleID =>
txt => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(txt),
utiOrBundleID
),
txt
);
};
// ------------ GENERICS FOR 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
});
// either :: (a -> c) -> (b -> c) -> Either a b -> c
const either = fl =>
// Application of the function fl to the
// contents of any Left value in e, or
// the application of fr to its Right value.
fr => e => "Left" in e
? fl(e.Left)
: fr(e.Right);
// fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
const fmapLR = f =>
// Either f mapped into the contents of any Right
// value in e, or e unchanged if is a Left value.
e => "Left" in e
? e
: Right(f(e.Right));
// MAIN ---
return main();
})();
The script copies the current TaskPaper selection (or all visible rows if the selection cursor is collapsed), creating a com.hogbaysoftware.bike.xml pasteboard item, ready for pasting into Bike.
(key=value or .class attributes in Bike rows can be made visible with custom style-sheets or extensions)
