Outline conversion - regex?

I regularly create mind maps that are n-levels deep. I want to convert them into an outline in another app that is flattened to three levels: topic, subheads & details. So level 0 and level 1 would stay the same, and levels 2-n would all be converted to level 2.

The mind map can be exported in outline form in a text file. The text file expresses the hierarchy as a return, a number of tabs for the outline level, and the outline node string.

So far, so good. All I have to do is search for the number of tabs which represent level n down to level 2 and replace them with the tabs that represent level 2.

Unfortunately, I don't know how to determine the level I'm supposed to start with -- level n. How do I figure out how many tabs is the maximum for the outline?

Also, is there a more elegant way to approach this conversion?

Any suggestions appreciated.

  1. Regex alone may not be the best approach, and
  2. absolutely feasible, but:
  • you need to show us a sample of typical input,
  • and you need to show us an example of exactly what output you need from that input.

To paste a text outline here, make sure that you enclose it between "markdown code fences" (triple back-ticks)

```
Alpha
    Beta
        Epsilon
        Zeta
        Eta
    Gamma
        Theta
        Iota
            Kappa
            Lambda
            Mu
        Nu
    Delta
        Xi
        Omicron
        Pi
```

You don't need to if use use a "repetition" feature of regex like this:

Search for this pattern: \t{2,}
and replace it with this pattern: \t\t

The search pattern says "match at least 2 tab characters". This match is then replaced with just 2 tabs.

Assuming the outline uses tabs to indent the text, effect is this:

Checkpoint ID: Before
--------------------
= LocalOutline =
0 Alpha
	1 Beta
		2 Epsilon
		2 Zeta
		2 Eta
	1 Gamma
		2 Theta
		2 Iota
			3 Kappa
			3 Lambda
			3 Mu
		2 Nu
	1 Delta
		2 Xi
		2 Omicron
		2 Pi

Checkpoint ID: After
--------------------
= LocalOutline =
0 Alpha
	1 Beta
		2 Epsilon
		2 Zeta
		2 Eta
	1 Gamma
		2 Theta
		2 Iota
		3 Kappa
		3 Lambda
		3 Mu
		2 Nu
	1 Delta
		2 Xi
		2 Omicron
		2 Pi

Here's the simple macro I used for this (thanks to @ComplexPoint's example outline in which I've replaced the spaces with tabs).
Test Flatten Tab Indented Text.kmmacros (1.9 KB)

Click to see macro

Keyboard Maestro Export

and here's a possible approach (some simple regular expressions, but embedded in a scripting language) which:

  • lets you vary the indentation limit,
  • and accepts either spaced or tabbed input indents of any consistent size (e.g. 2 space or 4 space indents), but always gives tabbed output indents.

Flattened text outline.kmmacros (7.4 KB)

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    // ----------- PARTIALLY FLATTENED OUTLINE -----------

    // Ver 0.01

    const main = () => {
        const
            kme = Application("Keyboard Maestro Engine"),
            kmVar = kme.getvariable,
            maxDepth = kmVar("maxOutlineDepth"),
            maxDepthInt = parseInt(maxDepth, 10);

        return isNaN(maxDepthInt) ? (
            Left(`Expected integer maxOutlineDepth, saw ${maxDepth}`)
        ) : (() => {
            const
                inputOutline = kmVar("inputOutline"),
                lineLevels = indentLevelsFromLines(
                    lines(inputOutline)
                ),
                minimumIndent = Math.min(
                    ...lineLevels.map(fst)
                );

            return lineLevels.map(
                    ([indent, text]) => {
                        const
                            n = Math.min(
                                maxDepth, indent - minimumIndent
                            );

                        return `${"\t".repeat(n)}${text}`;
                    }
                )
                .join("\n");
        })();
    };

    // -------------------- OUTLINES ---------------------

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

    // Left :: a -> Either a b
    const Left = x => ({
        type: "Either",
        Left: x
    });

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

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

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

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

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


    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];


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

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


    // sj :: a -> String
    const sj = (...args) =>
        // 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.
        JSON.stringify.apply(
            null,
            1 < args.length && !isNaN(args[0]) ? [
                args[1], null, args[0]
            ] : [args[0], null, 2]
        );

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

Thank you both. These are tremendously helpful.