DEVONthink Copy Selections as Text Outline Macro

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();
})();
2 Likes