By default, outlines copied from Bike are pasted into Apple Mail with bullets.
This variant Copy As
copies the Bike outline, with all its inline formatting, in Apple Mail’s native bullet-free outline format, which responds, in Mail, to:
Format > Indentation > Increase
⌘]Format > Indentation > Decrease
⌘[
To test the script below in Script Editor, set the language selector at top left to JavaScript.
Expand disclosure triangle to view JS source
(() => {
"use strict";
// Bike Outliner script :: selected rows copied as
// Apple Mail bullet-free rich text outline.
// Uses Mail indentation as in:
// Format > Indentation > Increase ( ⌘] )
// Format > Indentation > Decrease ( ⌘[ )
// Rob Trew @2022
// Ver 0.02
ObjC.import("AppKit");
const unWrap = ObjC.unwrap;
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return either(
alert("Copy As bullet-free Mail outline")
)(
compose(
() => "Copied as bullet-free Mail outline.",
setClipOfTextType("public.html")
)
)(
doc.exists() ? (
fmapLR(topLevelRowsInParse)(
dictFromHTML(
doc.export({
from: doc.rows.where({
selected: true
}),
as: "bike format",
all: false
})
)
)
) : Left("No documents open in Bike")
);
};
// ------------- APPLE MAIL HTML OUTLINE -------------
// divBlockHTML :: [Node Dict] -> String
const divBlockHTML = xs => {
// go :: Dict -> [String]
const go = liDict => {
const
pairOrSingle = liDict.nest,
xml = pairOrSingle[0].root.xml,
div = `<div>${xml}</div>`;
return 1 < pairOrSingle.length ? [
div,
(() => {
const
tag = "blockquote",
subTree = divBlockHTML(
pairOrSingle[1].nest
);
return `<${tag}>${subTree}</${tag}>`;
})()
] : [div];
};
return unlines(xs.flatMap(go));
};
// ----------------- XML PARSE TREE ------------------
// dictFromHTML :: String -> Either String Tree Dict
const dictFromHTML = html => {
const
error = $(),
node = $.NSXMLDocument.alloc
.initWithXMLStringOptionsError(
html, 0, error
);
return Boolean(error.code) ? (
Left("Not parseable as XML: " + (
`${html}`
))
) : Right(xmlNodeDict(node));
};
// topLevelRowsInParse :: Dict -> Tree Dict
const topLevelRowsInParse = dict =>
// Subforest of the XML parse tree
// corresponding to top-leveloutline rows.
divBlockHTML(
// parse.html.head.body.rootUL.nest
dict.nest[0].nest[1].nest[0].nest
);
// xmlNodeDict :: NSXMLNode -> Node Dict
const xmlNodeDict = xmlNode => {
const
hasChildren = 0 < parseInt(
xmlNode.childCount, 10
);
return Node({
name: unWrap(xmlNode.name),
content: hasChildren ? (
undefined
) : (unWrap(xmlNode.stringValue) || " "),
attributes: (() => {
const attrs = unWrap(xmlNode.attributes);
return Array.isArray(attrs) ? (
attrs.reduce(
(a, x) => Object.assign(a, {
[unWrap(x.name)]: unWrap(
x.stringValue
)
}),
{}
)
) : {};
})(),
xml: unWrap(xmlNode.XMLString)
})(
hasChildren ? (
unWrap(xmlNode.children)
.map(xmlNodeDict)
) : []
);
};
// ----------------------- 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
);
};
// --------------------- GENERIC ---------------------
// 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
});
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => 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 => e.Left ? (
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));
// unlines :: [String] -> String
const unlines = xs =>
// A single string formed by the intercalation
// of a list of strings with the newline character.
xs.join("\n");
// MAIN ---
return main();
})();
See: Using Scripts - Bike
For a Keyboard Maestro version, binding the script, by default, to ^⇧C, see: