In the meanwhile, for initial experimentation on scratch texts, rather than use on real documents, a couple of Keyboard Maestro macros:
togglingAndUpdatingTOCtags.kmmacros.zip (4.9 KB)
- Toggle a
@toc
tag on or off in selected Bike rows (initially ⌘T)
- Update all
@toc
tag values to contain outline numbering based on their position. (initially ⌘U)
Sample of a document containing updated @toc
tags:
Expand disclosure triangle to view a sample text
Welcome to Bike!
Use Bike to record and process your ideas. You'll need to spend a little time learning Bike to get the most out of it.
Bike Home Page @toc(1)
Watch intro movie
Glance through features list
https://www.hogbaysoftware.com/bike
Bike User's Guide @toc(2)
Read Getting Started section
Glance through other sections for future reference
https://bikeguide.hogbaysoftware.com
Keyboard Shortcuts @toc(3)
Row @toc(3.1)
Create Row: Return @toc(3.1.1)
Delete Row: Escape to outline mode, then Delete @toc(3.1.2)
Indent Row: Tab or Control-Command-Right @toc(3.1.3)
Unindent Row: Shift-Tab or or Control-Command-Left @toc(3.1.4)
Move Row Up: Control-Command-Up @toc(3.1.5)
Move Row Down: Control-Command-Down @toc(3.1.6)
Outline @toc(3.2)
Focus In: Option-Command-Right @toc(3.2.1)
Focus Out: Option-Command-Left
Expand Row: Command-0 or Escape to outline mode, then Right
Collapse Row: Command-9 or Escape to outline mode, then Left @toc(3.2.2)
Expand All Rows: Control-Command-0
Collapse All Rows: Control-Command-9
More @toc(3.3)
Toggle text/outline mode: Escape
Close Find Panel: Escape
Close Check Panel Escape
It is also possible to use these scripts without Keyboard Maestro, for example by using something like FastScripts to bind them to keystrokes.
If you test them in Script Editor.app
, you would to set the language selector at top left to JavaScript
rather than AppleScript
.
See: Using Scripts in Bike
Updating the outline-numbering values of any @toc
tags in the document, for example after:
- moving paragraphs, or
- adding more
@toc
tags.
Expand disclosure triangle to view JS Source – Updating @toc values
(() => {
"use strict";
// Updating the value of any @ord tags found
// with outline numbering strings based on their
// position in the outline.
// Rob Trew @2022
// Ver 0.01
const tagPrefix = "@toc";
// main :: IO ()
const main = () => {
const doc = Application("Bike").documents.at(0);
return doc.exists() ? (() => {
const
taggedRows = doc.rows.where({
name: {
_contains: tagPrefix
}
});
return rowsUpdated(tagPrefix)(doc)(
taggedNodesInForest(tagPrefix)(
partiallyOutlineNumberedForest(
Math.min(...taggedRows.level())
)(tagPrefix)([
Node({
text: "virtualRoot"
})(
bikeDocForestWithIDs(doc)
)
])
)
);
})() : "No document open in Bike.app";
};
// rowsUpdated :: String -> Bike Document ->
// [Dict] -> IO String
const rowsUpdated = tagPrefix =>
// Updates of the row.name() properties in rows
// identified by the `id` value in each node.
// In addition to the update effects, the function
// returns a listing of the updated texts.
doc => nodes => {
const rgxTag = new RegExp(
`(${tagPrefix}\\(.*\\)|${tagPrefix})`,
"gu"
);
return nodes.map(dict => {
const
newText = updatedText(rgxTag)(
tagPrefix
)(dict.text)(dict.ords),
row = doc.rows.byId(dict.id);
return (
row.name = newText,
newText
);
})
.join("\n");
};
// taggedNodesInForest :: String ->
// Forest Dict -> [Dict]
const taggedNodesInForest = tag =>
// A list of those nodes in the given forest
// which contain the specified tag.
forest => forest.flatMap(
foldTree(
x => xs => x.text.includes(tag) ? (
[x].concat(xs.flat())
) : xs.flat()
)
);
// updatedTag :: Regex -> String -> String ->
// [Int] -> String
const updatedText = tagRegex =>
// Node text in which the value of the given tag has
// been updated with a dot-delimited outline label.
tag => txt => ns => txt.replace(
tagRegex,
`${tag}(${ns.join(".")})`
);
// partiallyOutlineNumberedForest :: Int -> String ->
// Forest {text::String} ->
// Forest {text::String, ords::[Int]}
const partiallyOutlineNumberedForest = topLevel =>
// A forest in which each node is decorated
// from a certain level downwards, with
// an outline-numbering list of integers.
tagPrefix => forest => {
const lastIncremented = xs =>
Boolean(xs.length) ? [
...xs.slice(0, -1),
1 + xs.slice(-1)[0]
] : [];
const go = level => ns => tree => {
const
dict = root(tree),
txt = dict.text,
labellingStarted = level >= topLevel;
return Tuple(
txt.includes(tagPrefix) ? (
lastIncremented(ns)
) : ns
)(
Node(
Object.assign({
ords: labellingStarted ? (
ns
) : []
},
dict
)
)(
mapAccumL(go(1 + level))(
labellingStarted ? (
[...ns, 1]
) : ns
)(nest(tree))[1]
)
);
};
return mapAccumL(
go(1)
)([])(forest)[1];
};
// ---------------------- BIKE -----------------------
// bikeDocForestWithIDs :: Bike Doc -> [Tree Dict]
const bikeDocForestWithIDs = doc => {
// A forest of strings representing the outline(s)
// in a Bike 1.3 document.
const rows = doc.rows;
return forestWithIDsFromIndentedLines(
zip(
rows.level()
)(
zip(
rows.id()
)(
rows.name()
)
)
);
};
// forestWithIDsFromIndentedLines :: [(Int, String)] ->
// [Tree {id: String, text:String, body:Int}]
const forestWithIDsFromIndentedLines = tuples => {
const go = xs =>
0 < xs.length ? (() => {
// First line and its sub-tree,
const [level, [id, text]] = xs[0],
[tree, rest] = span(x => level < x[0])(
xs.slice(1)
);
return [
Node({
id,
text,
level
})(go(tree))
]
// followed by the rest.
.concat(go(rest));
})() : [];
return go(tuples);
};
// --------------------- GENERIC ---------------------
// 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 || []
});
// 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];
}
}
}
});
// 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(
root(tree)
)(
nest(tree).map(go)
);
return go;
};
// mapAccumL :: (acc -> x -> (acc, y)) -> acc ->
// [x] -> (acc, [y])
const mapAccumL = f =>
// A tuple of an accumulation and a list
// obtained by a combined map and fold,
// with accumulation from left to right.
acc => xs => [...xs].reduce(
([a, bs], x) => second(
v => bs.concat(v)
)(
f(a)(x)
),
Tuple(acc)([])
);
// nest :: Tree a -> [a]
const nest = tree => {
// Allowing for lazy (on-demand) evaluation.
// If the nest turns out to be a function –
// rather than a list – that function is applied
// here to the root, and returns a list.
const xs = tree.nest;
return "function" !== typeof xs ? (
xs
) : xs(root(tree));
};
// root :: Tree a -> a
const root = tree =>
// The value attached to a tree node.
tree.root;
// second :: (a -> b) -> ((c, a) -> (c, b))
const second = f =>
// A function over a simple value lifted
// to a function over a tuple.
// f (a, b) -> (a, f(b))
([x, y]) => [x, f(y)];
// span :: (a -> Bool) -> [a] -> ([a], [a])
const span = p =>
// Longest prefix of xs consisting of elements which
// all satisfy p, tupled with the remainder of xs.
xs => {
const i = xs.findIndex(x => !p(x));
return -1 !== i ? (
Tuple(xs.slice(0, i))(
xs.slice(i)
)
) : Tuple(xs)([]);
};
// zip :: [a] -> [b] -> [(a, b)]
const zip = xs =>
// The paired members of xs and ys, up to
// the length of the shorter of the two lists.
ys => Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => [xs[i], ys[i]]);
// MAIN --
return main();
})();
Toggling @toc
tags on and off in selected rows:
Expand disclosure triangle to view JS Source – @toc toggle
(() => {
"use strict";
// `@toc` tag toggled in selected and non-empty
// lines of a Bike.app (1.3.1 Preview) outline.
// Rob Trew @2022
// Ver 0.03
// main :: IO ()
const main = () => {
// ------------------- OPTIONS -------------------
const tagName = "toc";
const tagValue = "";
// ------------------- TOGGLE --------------------
const
bike = Application("Bike"),
doc = bike.documents.at(0);
return doc.exists() ? (() => {
const
selectedRows = doc.rows.where({
selected: true,
_not: [{
name: ""
}]
});
return Boolean(selectedRows.length) ? (() => {
const
tagRegex = new RegExp(
`\\s+(@${tagName}\\(.*\\)|@${tagName})`,
"gu"
),
isTagged = Boolean(
tagRegex.exec(
selectedRows.at(0).name()
)
),
updated = isTagged ? (
clearTag(tagRegex)
) : addTag(tagRegex)(tagName)(tagValue);
return (
selectedRows().forEach(
row => row.name = updated(
row.name()
)
),
isTagged ? (
`@${tagName} cleared`
) : `Tagged @${tagName}`
);
})() : "Nothing selected in Bike";
})() : "No documents open in Bike";
};
// ---------------------- TAGS -----------------------
// clearTag :: Regex -> String -> String
const clearTag = tagRegex =>
rowText => rowText.replace(tagRegex, "");
// addTag :: String -> String -> String -> String
const addTag = tagRegex =>
tagName => tagValue => txt => {
const
affix = Boolean(tagValue.length) ? (
`@${tagName}(${tagValue})`
) : `@${tagName}`;
return `${clearTag(tagRegex)(txt)} ${affix}`;
};
// --------------------- GENERIC ---------------------
// iso8601Local :: Date -> String
const iso8601Local = dte =>
new Date(dte - (6E4 * dte.getTimezoneOffset()))
.toISOString();
// taskPaperDateString :: Date -> String
const taskPaperDateString = dte => {
const [d, t] = iso8601Local(dte).split("T");
return [d, t.slice(0, 5)].join(" ");
};
// MAIN ---
return main();
})();