Paste text outline to Mail.app in `Format > Indentation` style

Tab-indented and space-indented outlines pasted directly into Mail.app with the default Edit > Paste do not respond to Mail's:

Format > Indentation >
    Decrease ⌘[
	Increase ⌘]

This macro pastes text outlines in Mail's own indentation format, making them compatible with the commands above.

Paste text outline to Mail.app in Format > Indentation style.kmmacros (12 KB)


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

    ObjC.import("AppKit");

    // Paste text outline to Mail.app
    // in Mail > Format > Indentation format
    // ( responds to Increase ⌘] and Decrease ⌘[ )


    // Rob Trew @2022
    // Ver 0.01

    // main :: IO ()
    const main = () =>
        either(
            alert("Paste as Mail.app indented")
        )(
            x => x
        )(
            bindLR(
                clipTextLR()
            )(
                compose(
                    Right,
                    setClipOfTextType("public.html"),
                    mailQuoteBlocksFromForest,
                    forestFromIndentedLines,
                    indentLevelsFromLines,
                    lines
                )
            )
        );


    // ------- INDENTED MAIL.APP HTML FROM FOREST --------

    // mailQuoteBlocksFromForest :: [Tree String] -> String
    const mailQuoteBlocksFromForest = forest => {
        const
            style = "style=\"margin: 0px 0px 0px 40px;\"",
            go = tree => {
                const xs = tree.nest;

                return [
                        `<div>${tree.root || "&nbsp;"}</div>`,
                        ...(
                            0 < xs.length ? [
                                [
                                    `<blockquote ${style}>`,
                                    xs.flatMap(go).join("\n"),
                                    "</blockquote>"
                                ]
                                .join("\n")
                            ] : []
                        )
                    ]
                    .join("\n");
            };

        return forest.flatMap(go).join("\n");
    };


    // ----------------------- JXA -----------------------

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };


    // clipTextLR :: () -> Either String String
    const clipTextLR = () => {
        // Either a message, (if no clip text is found),
        // or the string contents of the clipboard.
        const
            v = ObjC.unwrap(
                $.NSPasteboard.generalPasteboard
                .stringForType($.NSPasteboardTypeString)
            );

        return Boolean(v) && 0 < v.length ? (
            Right(v)
        ) : Left("No utf8-plain-text found in clipboard.");
    };


    // setClipOfTextType :: String -> String -> IO String
    const setClipOfTextType = utiOrBundleID =>
        txt => {
            const pb = $.NSPasteboard.generalPasteboard;

            return (
                pb.clearContents,
                pb.setStringForType(
                    $(txt),
                    utiOrBundleID
                ),
                txt
            );
        };


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


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


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


    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = lr =>
        // Bind operator for the Either option type.
        // If lr has a Left value then lr unchanged,
        // otherwise the function mf applied to the
        // Right value in lr.
        mf => "Left" in lr ? (
            lr
        ) : mf(lr.Right);


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


    // 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]) => Tuple(f(x))(y);


    // forestFromIndentedLines :: [(Int, String)] ->
    // [Tree String]
    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)
                    );

                // followed by the rest.
                return [
                    Node(body)(go(tree))
                ].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));
    };


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


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