Scrivener-style compilation of drafts from DT group and record outlines?

Scrivener is a very good piece of work, but perhaps less scriptable (and more RTF focused) than is ideal for some workflows.

Has anyone experimented with:

  • building document outlines from groups and (unsorted sequences of) DEVONthink text records,
  • and then compiling a Markdown draft version of those outlines by traversing them with scripts ?

Roughly this kind of thing, I suppose, if we select a few groups to be treated as top level headers:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const dt3 = Application('DEVONThink 3');

        return mdHeadingsFromTextTreeForest(
            dtTextForest(dt3.selection())
        );
    };

    // ---------------- MARKDOWN HEADINGS ----------------

    // mdHeadingsFromTextTreeForest :: 
    // [Tree {name::String, text::String}] -> String
    const mdHeadingsFromTextTreeForest = forest =>
        unlines(
            forest.flatMap(
                mdHeadingsFromNameTextTree(1)
            )
        );

    // mdHeadingsFromNameTextTree :: Int ->
    // Tree {name::String, text::String} -> String
    const mdHeadingsFromNameTextTree = startLevel =>
        tree => {
            const go = level =>
                node => {
                    const
                        dict = node.root,
                        text = dict.text;
                    return [
                        `\n${'#'.repeat(level)} ${dict.name}\n`
                    ].concat(
                        Boolean(text) ? (
                            [text]
                        ) : []
                    ).concat(
                        node.nest.flatMap(
                            go(1 + level)
                        )
                    );
                }
            return go(startLevel)(tree);
        };


    // ------------------- DEVONTHINK --------------------

    // dtTextTree :: [Item] -> 
    // [Tree {name::String, text::String}]
    const dtTextForest = peers =>
        peers.map(
            fmapPureDT(
                x => ({
                    name: x.name(),
                    text: 'text' === x.kind() ? (
                        x.plainText()
                    ) : ''
                })
            )
        );


    // fmapPureDT :: (DTItem -> a) -> DTItem  -> Tree a
    const fmapPureDT = f => {
        // f mapped over x and each of its descendants,
        // with the resulting values held in a generic
        // tree structure.
        const go = x =>
            Node(f(x))(
                x.children()
                .map(go)
            );
        return go;
    };


    // --------------------- 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
    });


    // bindLR (>>=) :: Either a -> 
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);


    // 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 => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;


    // sj :: a -> String
    function sj() {
        // Abbreviation of showJSON for quick testing.
        // Default indent size is two, which can be
        // overriden by any integer supplied as the
        // first argument of more than one.
        const args = Array.from(arguments);
        return JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );
    }

    // unlines :: [String] -> String
    const unlines = xs =>
        // A single string formed by the intercalation
        // of a list of strings with the newline character.
        xs.join('\n');

    // MAIN ---
    return main();
})();

Not sure that’s what you have in mind however take a look at @ngan’s Script: Refreshable/Portable merged view of files in mixed formats + direct[almost] editing/addition of source files + dynamically linked to the contents of groups/tags

1 Like