A macro for Jesse Grosjean's Bike Outliner.
Creates a new footnote in Bike for the current cursor position:
- Inserts a link to a new note
- Creates the new note at the end of a named
footnotes section. (Automatically created if not found) - Selects the new note, giving focus to the footnotes section.
(To jump back and forth between the footnote and the row which contains a link to it, see a companion macro in Bike) which provides bidirectional linking in Bike)
BIKE – Add linked footnote for current cursor position.kmmacros (34 KB)
Expand disclosure triangle to view JS source
return (() => {
"use strict";
ObjC.import("AppKit");
// Create a new footnote:
// 1. Insert a link to a new note at the current cursor
// position.
// 2. Create the new note at the end of a named
// footnotes section. (Automatically created if not found)
// 3. Select the new note, giving focus to the footnote section.
// A companion script for:
// [Bidirectional linking](
// https://forum.keyboardmaestro.com/t/bike-outliner-bidirectional-linking-jumping-back-and-forth-between-link-and-target/35804
// )
// which jumps back and forth between such footnotes and the
// links which point to them.
// Rob Trew @2024
// Ver 0.01
// ---------------------- MAIN -----------------------
const main = () => {
const title = "Insert link to new footnote.";
const footnoteSectionName = "Footnotes";
const inlineFootnoteMark = "*";
const
bike = Application("Bike"),
frontDoc = bike.documents.at(0);
return either(
alert(title)
)(
notify(title)("")("pop")
)(
bindLR(
footnoteMarkCheckedLR(inlineFootnoteMark)
)(
compose(
bindLR(
frontDoc.exists()
? Right(frontDoc)
: Left("No document open in Bike.")
),
newNoteAndLinkLR(footnoteSectionName)(
bike
)
)
)
);
};
// footnoteMarkCheckedLR :: String -> Either String String
const footnoteMarkCheckedLR = mark =>
0 < mark.length
? Right(mark)
: Left(
[
"Inline footnote mark can be a space, ",
"or a visible character like ^,",
"but not an empty string."
]
.join("\n")
);
// newNoteAndLinkLR :: String -> Bike Application ->
// Bike Document -> Either IO String IO String
const newNoteAndLinkLR = footnoteSectionName =>
bike => inlineFootnoteMark => doc => {
const
firstSelectedRow = doc.rows
.where({selected: true})
.at(0);
return bindLR(
firstSelectedRow.exists()
? Right(firstSelectedRow)
: Left(
`Nothing selected in ${doc.name()}`
)
)(
() => newFootnoteAddedLR(bike)(doc)(
inlineFootnoteMark
)(footnoteSectionName)
);
};
// newFootnoteAddedLR :: Bike Application ->
// Bike Document -> String ->
// String -> IO Either String String
const newFootnoteAddedLR = app =>
doc => footnoteLinkMark => footNotesSectionName => {
const
footNoteParent = namedRowFoundOrCreated(app)(
footNotesSectionName
)(doc);
return bindLR(
newNoteChildLR(doc)(footNoteParent)("")
)(
footNoteRow => (
// Effects,
copyTypedString(true)(
"com.hogbaysoftware.bike.xml"
)(
linkXML(
doc.id()
)(
footNoteParent.id()
)(
footnoteLinkMark
)(
footNoteRow.id()
)
),
menuItemClicked("Bike")([
"Edit", "Paste", "Paste"
]),
doc.select({at: footNoteRow}),
doc.focusedRow = footNoteParent,
// Value.
Right("New footnote selected.")
)
);
};
// linkXML :: String -> String -> String ->
// String -> XML String
const linkXML = docID =>
parentID => mark => footNoteID => {
const url = `bike://${docID}/${parentID}#${footNoteID}`;
return (
`<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta charset="utf-8"/></head>
<body><ul><li>
<p><a href="${url}">${mark}</a></p>
</li></ul></body>
</html>`
);
};
// namedRowFoundOrCreated :: Bike Application ->
// String -> Bike Document -> Bike Row
const namedRowFoundOrCreated = app =>
name => doc => {
const
firstExisting = doc.rows.where({name})
.at(0);
return firstExisting.exists()
? firstExisting
: (() => {
const newRow = new app.Row({name});
return (
doc.rows.push(newRow),
newRow
);
})();
};
// newNoteChildLR :: Bike Doc -> Bike Row ->
// String -> IO Either String Bike Row
const newNoteChildLR = doc =>
parentRow => name => parentRow.exists()
? (() => {
const
addedRows = doc.import({
from: footNoteXML(name),
to: parentRow,
as: "bike format"
});
return 0 < addedRows.length
? Right(addedRows[0])
: Left(`New row not added: '${name}'`);
})()
: Left("Parent row not found in noteChildAddedLR");
// footNoteXML :: String -> XML String
const footNoteXML = name =>
`<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta charset="utf-8"/></head>
<body>
<ul><li data-type="note">
<p>${name}</p>
</li></ul>
</body>
</html>`;
// ----------------------- 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
);
};
// copyTypedString :: Bool -> String -> String -> IO ()
const copyTypedString = blnClear =>
// public.html, public.rtf, public.utf8-plain-text
pbUTI => s => {
const pb = $.NSPasteboard.generalPasteboard;
return (
blnClear && pb.clearContents,
pb.setStringForType(
$(s),
$(pbUTI)
),
s
);
};
// menuItemClicked :: String -> [String] -> IO Bool
const menuItemClicked = appName =>
// Click an OS X app sub-menu item
// 2nd argument is an array of arbitrary length
// (exact menu item labels, giving full path)
menuItems => {
const nMenuItems = menuItems.length;
return nMenuItems > 1
? (() => {
const
appProcs = Application("System Events")
.processes.where({
name: appName
});
return 0 < appProcs.length
? (
Application(appName)
.activate(),
menuItems.slice(1, -1)
.reduce(
(a, x) => a.menuItems[x]
.menus[x],
appProcs[0].menuBars[0]
.menus.byName(menuItems[0])
)
.menuItems[menuItems[nMenuItems - 1]]
.click(),
true
)
: false;
})()
: false;
};
// notify :: String -> String -> String ->
// String -> IO ()
const notify = withTitle =>
subtitle => soundName => message =>
Object.assign(
Application.currentApplication(),
{includeStandardAdditions: true}
)
.displayNotification(
message,
{
withTitle,
subtitle,
soundName
}
);
// --------------------- GENERIC ---------------------
// Left :: a -> Either a b
const Left = x => ({
type: "Either",
Left: x
});
// Right :: b -> Either a b
const Right = x => ({
type: "Either",
Right: x
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = lr =>
// Bind operator for the Either option type.
// If lr has a Left value then lr unchanged,
// otherwise the function mf applied to the
// Right value in lr.
mf => "Left" in lr
? lr
: mf(lr.Right);
// 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 => "Left" in e
? fl(e.Left)
: fr(e.Right);
// MAIN ---
return main();
})();