A temporary script while (Preview 69) has blued text with hyperlinks but not yet a “look before you leap” tooltip to visually check/inspect the URL with.
(Displays a dialog showing the link(s) behind blue text in the current line, and offers to open one or all of them, or Cancel)
BIKE - Inspect link url(s) behind blue text in current row before opening.kmmacros.zip (5.5 KB)
Expand disclosure triangle to view JS source
(() => {
"use strict";
// BIKE (Preview 69) draft script.
// "Look before leap" for any links in current row.
// Display a dialog showing Title and URL for
// each link (if any) in the current Bike row,
// offering a chance to open one or all, or cancel.
// Rob Trew @2022
// Ver 0.06
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (
either(
alert("Links in current row")
)(xs => {
const n = xs.length;
return 0 < n ? (
1 < n ? (
showLinks(xs)
) : showSingleLink(xs[0])
) : "No links found in current row.";
})(
titleLinkPairsFromBikeRowHtmlLR(
doc.selectionRow.htmlContent()
)
)
) : "No documents open in Bike";
};
// -------------- LINKS IN CURRENT ROW ---------------
// showLinks :: [(String, URL String)] -> IO ()
const showLinks = xs =>
0 < xs.length ? (() => {
const
se = Object.assign(
Application("System Events"),
{includeStandardAdditions: true}
),
links = xs.map(snd),
choices = (
se.activate(),
se.chooseFromList(links, {
withTitle: "Links in row",
withPrompt: xs.map(
(x, i) => `${1 + i}. ${x[0]}`
).join("\n"),
defaultItems: [links[0]],
okButtonName: "Open",
cancelButtonName: "Cancel",
multipleSelectionsAllowed: true,
emptySelectionAllowed: true
})
);
return Boolean(choices) ? (
Object.assign(
Application.currentApplication(),
{includeStandardAdditions: true}
).doShellScript(
choices.map(k => `open ${k}`)
.join("\n")
)
) : "No link chosen.";
})() : "'showLinks' undefined for empty list.";
// showSingleLink :: (String, URL String) -> IO ()
const showSingleLink = ([title, url]) => {
// Display, and choice of following or ignoring,
// a single (Title, URL) pair.
const
se = Object.assign(
Application("System Events"),
{includeStandardAdditions: true}
),
legend = `Name: ${title}\n\nLink: ${url}`;
try {
return (
se.activate(),
se.displayDialog(legend, {
buttons: ["Cancel", "Open"],
defaultButton: "Open",
withTitle: (
"First link in selected Bike row"
),
givingUpAfter: 30
}),
Object.assign(
Application.currentApplication(),
{includeStandardAdditions: true}
).doShellScript(`open ${url}`),
`Opening: ${url}`
);
} catch (e) {
return e.message;
}
};
// ----------------------- XML -----------------------
// titleLinkPairsFromBikeRowHtmlLR ::
// HTML String -> Either String [(String, String)]
const titleLinkPairsFromBikeRowHtmlLR = rowHTML =>
// A sequence of plain text (Title, URL) pairs
// extracted from any inline formatting in
// the HTML of a Bike rows.
fmapLR(
compose(
map(x => [
contentFromNest(x),
x[0].root.attributes.href
]),
groupBy(on(eq)(
v => v.root.attributes.href
)),
treeMatches(dct => "a" === dct.name)
)
)(
dictFromHTML(rowHTML)
);
// 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));
};
// contentFromNest :: [Node Dict] -> String
const contentFromNest = forest =>
// Concatenation of any descendent content attributes.
forest.map(
t => foldTree(x => vs => {
const maybeText = x.content;
return Boolean(maybeText) ? (
[maybeText].concat(vs.flat())
) : vs.flat();
})(t).join("")
).join("");
// 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
});
// 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];
}
}
}
});
// 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);
// eq (==) :: Eq a => a -> a -> Bool
const eq = a =>
// True when a and b are equivalent in the terms
// defined below for their shared data type.
b => a === b;
// 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;
};
// groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
const groupBy = eqOp =>
// A list of lists, each containing only elements
// equal under the given equality operator,
// such that the concatenation of these lists is xs.
xs => Boolean(xs.length) ? (() => {
const [h, ...t] = xs;
const [groups, g] = t.reduce(
([gs, a], x) => eqOp(x)(a[0]) ? (
Tuple(gs)([...a, x])
) : Tuple([...gs, a])([x]),
Tuple([])([h])
);
return [...groups, g];
})() : [];
// map :: (a -> b) -> [a] -> [b]
const map = f =>
// The list obtained by applying f
// to each element of xs.
// (The image of xs under f).
xs => [...xs].map(f);
// on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
const on = f =>
// e.g. groupBy(on(eq)(length))
g => a => b => f(g(a))(g(b));
// snd :: (a, b) -> b
const snd = tpl =>
// Second member of a pair.
tpl[1];
// treeMatches :: (a -> Bool) -> Tree a -> [Tree a]
const treeMatches = p => {
// A list of all nodes in the tree which match
// a predicate p.
// For the first match only, see findTree.
const go = tree =>
p(tree.root) ? (
[tree]
) : tree.nest.flatMap(go);
return go;
};
// MAIN ---
return main();
})();
To test in Script Editor, set language selector at top left to JavaScript
.
See: Using Scripts - Bike