The text copied by DEVONthink's default Edit > Copy
(from selections in List View outlines) is less useful than it might be.
Copying a multiple selection like this, for example:
only gets us a slightly disappointing single line of text in the Clipboard:
Deep Simplicity: Chaos, Complexity and the Emergence of Life (Penguin Press Science), John Gribbin
Here is a macro (with some adjustable options) which copies as an indented text outline (defaulting to TaskPaper format).
i.e. the selection above copies as:
- Deep Simplicity: Chaos, Complexity and the Emergence of Life (Penguin Press Science), John Gribbin
- Image of the universe
- (@L4143) Chaos and complexity combine to make the Universe a very orderly place, just right for life-forms like us. As Stuart Kauffman has put it,
- ImageofTheUniverse.graffle
- Modelling atoms is easier
- (@L210) It is no surprise that the most complex features of the Universe, which proved most reluctant to yield to the traditional methods of scientific investigation,
- AtomsEasierToModel.graffle
DEVONthink Copy Selections as Text Outline.kmmacros (26 KB)
Expand disclosure triangle to view JS Source
(() => {
"use strict";
// Names of all selected DEVONthink records/folders
// in indented outline format.
// Rob Trew @2021
// Ver 0.01
// ----- TEXT OUTLINE FROM DEVONTHINK SELECTIONS -----
// main :: IO ()
const main = () => {
// OPTIONS
// Full path from top level enclosing folder,
// or just path from highest selected level ?
const boolFullPath = false;
// Indentation unit, e.g. tab or 4 spaces
const indentUnit = "\t";
// Line format function, e.g. addition of prefix
const lineFormat = s => `- ${s}`;
const
fullPaths = Application("DevonThink 3")
.selectedRecords()
.map(
rec => (
rec.location().slice(1) + rec.name()
)
.split("/")
),
minLength = minimum(
fullPaths.map(x => x.length)
),
selectedPaths = fullPaths.map(
drop(minLength - 1)
);
return outlineFromForest(indentUnit)(lineFormat)(
// Nested tree of texts, derived from
// location strings (and names) of selected
// records and folders.
(
boolFullPath ? (
fullPaths
) : selectedPaths
)
.reduce(addPath, [])
);
};
// ------------------ GENERIC TREES ------------------
// addPath :: (Forest String, [String]) -> Forest String
const addPath = (forest, path) => {
// A forest of strings to which an
// additional path has been added.
const go = (n, trees, xs) =>
0 < n ? (() => {
const s = xs[0];
return maybe(
// Just a new tree if this location
// path has not yet been seen.
trees.concat(
Node(s)(
go(n - 1, [], xs.slice(1))
)
)
)(
// Or an expanded tree if the location
// path so far already exists.
i => trees.slice(0, i)
.concat(
Node(s)(
go(
n - 1,
trees[i].nest,
xs.slice(1)
)
)
)
.concat(trees.slice(1 + i))
)(
findIndex(
x => s === x.root
)(trees)
);
})() : trees;
return go(path.length, forest, path);
};
// outlineFromForest :: String ->
// (a -> String) -> [Tree a] -> [String]
const outlineFromForest = unitIndent =>
// Indented text representation of a list of Trees.
// f is an (a -> String) function defining
// the string representation of tree nodes.
f => trees => {
const go = indent => x => {
const
s = indent + f(x.root),
xs = x.nest,
nextDepth = unitIndent + indent;
return 0 < xs.length ? (
[s].concat(
xs.flatMap(go(nextDepth))
)
) : s;
};
return trees.flatMap(go(""))
.join("\n");
};
// --------------------- GENERIC ---------------------
// Just :: a -> Maybe a
const Just = x => ({
type: "Maybe",
Nothing: false,
Just: 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 || []
});
// Nothing :: Maybe a
const Nothing = () => ({
type: "Maybe",
Nothing: true
});
// drop :: Int -> [a] -> [a]
// drop :: Int -> String -> String
const drop = n =>
xs => xs.slice(n);
// findIndex :: (a -> Bool) -> [a] -> Maybe Int
const findIndex = p =>
// Just the index of the first element in
// xs for which p(x) is true, or
// Nothing if there is no such element.
xs => {
const i = [...xs].findIndex(p);
return -1 !== i ? (
Just(i)
) : Nothing();
};
// minimum :: Ord a => [a] -> a
const minimum = xs => (
// The least value of xs.
ys => 0 < ys.length ? (
ys.slice(1)
.reduce((a, y) => y < a ? y : a, ys[0])
) : null
)(xs);
// maybe :: b -> (a -> b) -> Maybe a -> b
const maybe = v =>
// Default value (v) if m is Nothing, or f(m.Just)
f => m => m.Nothing ? (
v
) : f(m.Just);
// MAIN ()
return main();
})();