While the GUI helpfully lets us look before we leap, (inspect a URL visually before we follow a link) there may be moments at the start of work when we want to batch-open all the links in all selected rows, without needing to visually inspect them first.
Here’s a Keyboard Maestro version, bound by default to ⌘J
Bike Outliner – Open all links in selected lines - Macro Library - Keyboard Maestro Discourse
and here’s the script itself, for testing in Script Editor.app (with language selector at top left set to JavaScript) or for using with other launchers, like FastScripts or Alfred.
See: Using Scripts - Bike
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();
})();