A macro for Jesse Grosjean's Bike Outliner.
Bike's GUI allows us to "look before we leap", and check a URL visuallly before we follow a link.
This is macro is for moments when we want to open a set of links quickly, are confident that we know where they all point and, are happy to leap before we look.
BIKE – Open all links in selected rows.kmmacros (28 KB)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
ObjC.import("AppKit");
// Directly open (leap without look)
// any links in the selected Bike rows.
// main : IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (() => {
const
message = alert(
"Open links in selected rows"
);
return either(message)(
xs => 0 < xs.length ? (() => {
const
links = nub(xs.map(
x => x.attributes.href
));
return Object.assign(
Application.currentApplication(), {
includeStandardAdditions: true
}
)
.doShellScript(
links.map(x => `open "${x}"`)
.join("\n")
),
links.join("\n");
})() : message(
"No links found in selected rows."
)
)(
fmapLR(
filterTree(
x => Boolean(x.attributes.href)
)
)(
dictFromHTML(
doc.export({
from: doc.rows.where({
selected: true
}),
as: "bike format",
all: false
})
)
)
);
})() : "No documents open in Bike";
};
// ----------------------- XML -----------------------
// 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));
};
// xmlNodeDict :: NSXMLNode -> Node Dict
const xmlNodeDict = xmlNode => {
const
unWrap = ObjC.unwrap,
blnChiln = 0 < parseInt(
xmlNode.childCount, 10
);
return Node({
name: unWrap(xmlNode.name),
content: blnChiln ? (
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
)
}),
{}
)
) : {};
})()
})(
blnChiln ? (
unWrap(xmlNode.children)
.reduce(
(a, x) => a.concat(xmlNodeDict(x)),
[]
)
) : []
);
};
// ----------------------- 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
);
};
// --------------------- 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
});
// 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);
// filterTree (a -> Bool) -> Tree a -> [a]
const filterTree = p =>
// List of all values in the tree
// which match the predicate p.
foldTree(x => xs =>
(
p(x) ? [
[x], ...xs
] : xs
).flat(1)
);
// 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));
// 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(
tree.root
)(
tree.nest.map(go)
);
return go;
};
// nub :: Eq a => [a] -> [a]
const nub = xs =>
[...new Set(xs)];
// MAIN ---
return main();
})();