Create a nested folder structure based on a plain text outline

Updated code and simplified behaviour – simply displays a tree with full paths of the created (or found) nest of folders.

(Now makes no distinction, in the display, between 'found' and 'created' – some users may have found this confusing)

Nested folders created (or found) from text outline.kmmacros (14 KB)


Expand disclosure triangle to view JS Source
// Create a folder structure, at a given 'anchor' filePath,
// based on a supplied plain text outline.
// (must be consistently tab or 4-space indented)

// Ver 0.03
(() => {
    "use strict";

    // main :: IO String
    const main = () => {
        const
            kme = Application("Keyboard Maestro Engine"),
            kmVar = kme.getvariable;

        return drawTree(
            fmapTree(
                compose(
                    either(x => x)(x => x),
                    createDirectoryIfMissingLR(true)
                )
            )(
                filePathTree(kmVar("anchorFolderPath"))(
                    forestFromIndentedLines(
                        indentLevelsFromLines(
                            lines(kmVar("folderOutline"))
                        )
                    )
                )
            )
        );
    };

    // ------------ TREES FROM INDENTED TEXT -------------

    // forestFromIndentedLines :: [(Int, String)] ->
    // [Tree {text:String, body:Int}]
    const forestFromIndentedLines = tuples => {
        const go = xs =>
            0 < xs.length ? (() => {
                // First line and its sub-tree,
                const [depth, body] = xs[0],
                    [tree, rest] = span(x => depth < x[0])(
                        xs.slice(1)
                    );

                return [
                        Node({
                            text: body,
                            level: depth
                        })(go(tree))
                    ]
                    // followed by the rest.
                    .concat(go(rest));
            })() : [];

        return go(tuples);
    };


    // indentLevelsFromLines :: [String] -> [(Int, String)]
    const indentLevelsFromLines = xs => {
        const
            pairs = xs.map(
                x => bimap(
                    cs => cs.length
                )(
                    cs => cs.join("")
                )(
                    span(isSpace)([...x])
                )
            ),
            indentUnit = pairs.reduce(
                (a, [i]) => 0 < i ? (
                    i < a ? i : a
                ) : a,
                Infinity
            );

        return [Infinity, 0].includes(indentUnit) ? (
            pairs
        ) : pairs.map(first(n => n / indentUnit));
    };


    // ---------------- GENERIC FUNCTIONS ----------------

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


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


    // bimap :: (a -> b) -> (c -> d) -> (a, c) -> (b, d)
    const bimap = f =>
        // Tuple instance of bimap.
        // A tuple of the application of f and g to the
        // first and second values respectively.
        g => tpl => Tuple(f(tpl[0]))(
            g(tpl[1])
        );


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


    // createDirectoryIfMissingLR :: Bool -> FilePath
    // -> Either String FilePath
    const createDirectoryIfMissingLR = blnParents =>
        dirPath => {
            const fp = filePath(dirPath);

            return doesPathExist(fp) ? (
                Right(fp)
            ) : (() => {
                const
                    e = $(),
                    blnOK = $.NSFileManager
                    .defaultManager[
                        "createDirectoryAtPath" + (
                            "WithIntermediateDirectories"
                        ) + "AttributesError"
                    ](fp, blnParents, void 0, e);

                return blnOK ? (
                    Right(fp)
                ) : Left(e.localizedDescription);
            })();
        };


    // doesPathExist :: FilePath -> IO Bool
    const doesPathExist = fp =>
        $.NSFileManager.defaultManager
        .fileExistsAtPath(
            $(fp).stringByStandardizingPath
        );


    // draw :: Tree String -> [String]
    const draw = node => {
        // shift :: String -> String -> [String] -> [String]
        const shifted = (first, other, xs) => (
            [first].concat(
                Array.from({
                    length: xs.length - 1
                }, () => other)
            ).map(
                (y, i) => y.concat(xs[i])
            )
        );
        // drawSubTrees :: [Tree String] -> [String]
        const drawSubTrees = xs => {
            const lng = xs.length;

            return 0 < lng ? (
                1 < lng ? (
                    ["│"].concat(
                        shifted("├─ ", "│  ", draw(xs[0]))
                    )
                ).concat(
                    drawSubTrees(xs.slice(1))
                ) : ["│"].concat(
                    shifted("└─ ", "   ", draw(xs[0]))
                )
            ) : [];
        };

        return node.root.split("\n").concat(
            drawSubTrees(node.nest)
        );
    };


    // drawTree :: Tree String -> String
    const drawTree = tree =>
        draw(tree).join("\n");


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


    // filePath :: String -> FilePath
    const filePath = s =>
        // The given file path with any tilde expanded
        // to the full user directory path.
        ObjC.unwrap(
            ObjC.wrap(s)
            .stringByStandardizingPath
        );


    // filePathTree :: filePath -> [Tree String] -> Tree FilePath
    const filePathTree = fpAnchor => trees => {
        const go = fp => tree => {
            const path = `${fp}/${tree.root.text}`;

            return Node(path)(
                tree.nest.map(go(path))
            );
        };

        return Node(fpAnchor)(
            trees.map(go(fpAnchor))
        );
    };


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => [f(x), y];


    // fmapTree :: (a -> b) -> Tree a -> Tree b
    const fmapTree = f => {
        // A new tree. The result of a
        // structure-preserving application of f
        // to each root in the existing tree.
        const go = t => Node(
            f(root(t))
        )(
            nest(t).map(go)
        );

        return go;
    };


    // isSpace :: Char -> Bool
    const isSpace = c =>
        // True if c is a white space character.
        (/\s/u).test(c);


    // lines :: String -> [String]
    const lines = s =>
        // A list of strings derived from a single string
        // which is delimited by \n or by \r\n or \r.
        Boolean(s.length) ? (
            s.split(/\r\n|\n|\r/u)
        ) : [];


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


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


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