A macro for Jesse Grosjean's Bike Outliner.
Bike's default Edit > Copy
places a clean unbulleted (tab-indented) outline in the clipboard (together with OPML and HTML outlines).
This macro provides a variant which copies either:
- the whole Bike document, or
- the selected rows (if the selection is extended)
as a bulleted plain text (tab-indented) outline.
BIKE - Copy as Bulleted Plain Text.kmmacros (6.3 KB)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
ObjC.import("AppKit");
// Copy Bike document
// (or Bike selected rows, if selected is extended)
// as bulleted plain text.
// RobTrew @2022
// Ver 0.01
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (
copyText(
bulletedTextFromBikeDoc(doc)
)
) : "No document open in Bike";
};
// ---------------------- BIKE -----------------------
// bulletedTextFromBikeDoc :: Bike Doc -> IO String
const bulletedTextFromBikeDoc = doc => {
const
rows = Boolean(doc.selectedText()) ? (
doc.rows.where({selected: true})
) : doc.rows;
return bulletOutlineFromForest(
forestFromIndentedLines(
zip(
rows.level()
)(
rows.name()
)
)
);
};
// ----------------------- JXA -----------------------
// copyText :: String -> IO String
const copyText = s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
pb.clearContents,
pb.setStringForType(
$(s),
$.NSPasteboardTypeString
),
s
);
};
// --------------------- GENERIC ---------------------
// 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 || []
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = a =>
// A pair of values, possibly of
// different types.
b => ({
type: "Tuple",
"0": a,
"1": b,
length: 2,
*[Symbol.iterator]() {
for (const k in this) {
if (!isNaN(k)) {
yield this[k];
}
}
}
});
// bulletOutlineFromForest :: Forest String -> String
const bulletOutlineFromForest = trees => {
const go = tabs => tree => {
const txt = tree.root.text;
return [
Boolean(txt) ? (
`${tabs}- ${txt}`
) : `${tabs}`,
...tree.nest.flatMap(go(`\t${tabs}`))
];
};
return trees.flatMap(go("")).join("\n");
};
// forestFromIndentedLines :: [(Int, String)] ->
// [Tree {text:String, body:Int}]
const forestFromIndentedLines = tuples => {
const go = xs =>
0 < xs.length ? (() => {
// First line and its sub-tree,
const [depth, body] = xs[0],
[tree, rest] = span(x => depth < x[0])(
xs.slice(1)
);
return [
Node({
text: body,
level: depth
})(go(tree))
]
// followed by the rest.
.concat(go(rest));
})() : [];
return go(tuples);
};
// span :: (a -> Bool) -> [a] -> ([a], [a])
const span = p =>
// Longest prefix of xs consisting of elements which
// all satisfy p, tupled with the remainder of xs.
xs => {
const i = xs.findIndex(x => !p(x));
return -1 !== i ? (
Tuple(xs.slice(0, i))(
xs.slice(i)
)
) : Tuple(xs)([]);
};
// zip :: [a] -> [b] -> [(a, b)]
const zip = xs =>
// The paired members of xs and ys, up to
// the length of the shorter of the two lists.
ys => Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => [xs[i], ys[i]]);
return main();
})();