Converting markdown tags to formatted text

web-based outliners such as Workflowy and Dynalist use markdown for formatting. When I import these .opml outlines into OO, italicized text appears between the tags and (bold would of course be and ). Is there an easy way to convert these to italicized text in OmniOutliner 5?

This seems exactly the kind that the omniJS interface should in principle be well suited to. I did try something like it with the JXA interface, but had to fall back on repeated use of a slowish .duplicate() method (couldn’t get .insert() to add rich text anywhere but at the start of a topic or note), and couldn’t figure out how to use the RichText constructor to directly produce the .paragraphs etc of note cells and topic cells.

The result sort of works, but is a bit too slow and cumbersome for real use, I think.

I personally mainly work in plain text – but I do recommend (MD -> Rich Text) as an omniJS project to anyone who likes to work in that medium. I have sketched a set of draft JS functions for doing a simple parse of inline MD (bold, italic, links, but not emphases within link labels), and for creating an array of text run-dictionaries for mapping through the API to the rich text. Happy to share those if anyone wants to experiment with that next stage in omniJS.

For pasting, rather than opml import, you could look at things like:

Some functions and a basic test of appending a line of Markdown (emphases, links) to the note of the selected row. This version uses the JXA/Applescript interface rather than the omniJS API, which might well execute faster.

(ES6 JS, so Sierra onwards)

Not really usable till the writing of Rich Text attribute runs is properly solved, and gets a bit quicker and less clumsy. There may well be a good clear route – but I just haven’t managed to find it.

(() => {
    'use strict';

    // Rob Trew (c) 2017 

        // Sketches of some functions towards (Markdown -> Rich Text)

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

        // Regex (needs g flag for multiples) -> HayStack -> [MatchParts]
        // allMatches :: Regex -> Text -> [Text]
        const allMatches = (rgx, strHay) =>
            unfoldr(
                a => {
                    const xs = a.exec(strHay);
                    return Boolean(xs) ? {
                        just: [xs],
                        new: a
                    } : {
                        nothing: true
                    };
                },
                rgx
            );

        // (++) :: [a] -> [a] -> [a]
        const append = (xs, ys) => xs.concat(ys);

        // concat :: [[a]] -> [a] | [String] -> String
        const concat = xs =>
            xs.length > 0 ? (() => {
                const unit = typeof xs[0] === 'string' ? '' : [];
                return unit.concat.apply(unit, xs);
            })() : [];

        // cons :: a -> [a] -> [a]
        const cons = (x, xs) => [x].concat(xs);

        // elem :: Eq a => a -> [a] -> Bool
        const elem = (x, xs) => xs.indexOf(x) !== -1;

        // even :: Integral a => a -> Bool
        const even = n => n % 2 === 0;

        // foldl :: (b -> a -> b) -> b -> [a] -> b
        const foldl = (f, a, xs) => xs.reduce(f, a);

        // headMay :: [a] -> Maybe a
        const headMay = xs =>
            xs.length > 0 ? {
                nothing: false,
                just: xs[0]
            } : {
                nothing: true
            };

        // isInfixOf :: Eq a => [a] -> [a] -> Bool
        const isInfixOf = (needle, haystack) =>
            haystack.includes(needle);

        // lastMay :: [a] -> Maybe a
        const lastMay = xs => xs.length > 0 ? {
            just: xs.slice(-1)[0]
        } : {
            nothing: true
        };

        // map :: (a -> b) -> [a] -> [b]
        const map = (f, xs) => xs.map(f);

        // mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
        const mapAccumL = (f, acc, xs) =>
            xs.reduce((a, x) => {
                const pair = f(a[0], x);
                return [pair[0], a[1].concat([pair[1]])];
            }, [acc, []]);

        // length :: [a] -> Int
        const length = xs => xs.length;

        // lines :: String -> [String]
        const lines = s => s.split(/[\r\n]/);

        // log :: a -> IO ()
        const log = (...args) =>
            console.log(
                args
                .map(JSON.stringify)
                .join(' -> ')
            );

        // odd :: Integral a => a -> Bool
        const odd = n => !even(n);

        // quotRem :: Integral a => a -> a -> (a, a)
        const quotRem = (m, n) => [Math.floor(m / n), m % n];

        // show :: Int -> a -> Indented String
        // show :: a -> String
        const show = (...x) =>
            JSON.stringify.apply(
                null, x.length > 1 ? [x[1], null, x[0]] : x
            );

        // snd :: (a, b) -> b
        const snd = tpl => Array.isArray(tpl) ? tpl[1] : undefined;

        // Splitting not on a delimiter, but whenever the relationship between
        // two consecutive items matches a supplied predicate function

        // splitBy :: (a -> a -> Bool) -> [a] -> [[a]]
        const splitBy = (f, xs) =>
            (xs.length < 2) ? [xs] : (() => {
                const
                    h = xs[0],
                    lstParts = xs.slice(1)
                    .reduce(([acc, active, prev], x) =>
                        f(prev, x) ? (
                            [acc.concat([active]), [x], x]
                        ) : [acc, active.concat(x), x], [
                            [],
                            [h],
                            h
                        ]);
                return lstParts[0].concat([lstParts[1]]);
            })();

        // splitOn :: a -> [a] -> [[a]]
        // splitOn :: String -> String -> [String]
        const splitOn = (needle, haystack) =>
            typeof haystack === 'string' ? (
                haystack.split(needle)
            ) : (function sp_(ndl, hay) {
                const mbi = findIndex(x => ndl === x, hay);
                return mbi.nothing ? (
                    [hay]
                ) : append(
                    [take(mbi.just, hay)],
                    sp_(ndl, drop(mbi.just + 1, hay))
                );
            })(needle, haystack);

        // splitAt :: Int -> [a] -> ([a],[a])
        const splitAt = (n, xs) => [xs.slice(0, n), xs.slice(n)];

        // splitEvery :: Int -> [a] -> [[a]]
        const splitEvery = (n, xs) => {
            if (xs.length <= n) return [xs];
            const [h, t] = [xs.slice(0, n), xs.slice(n)];
            return [h].concat(splitEvery(n, t));
        }

        // takeWhile :: (a -> Bool) -> [a] -> [a]
        const takeWhile = (f, xs) => {
            for (var i = 0, lng = xs.length;
                (i < lng) && f(xs[i]); i++) {}
            return xs.slice(0, i);
        };

        // unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
        const unfoldr = (mf, v) => {
            let xs = [];
            return (until(
                m => m.nothing,
                m => {
                    const m2 = mf(m.new);
                    return (
                        xs = m2.nothing ? xs : xs.concat(m2.just),
                        m2
                    );
                }, {
                    nothing: false,
                    just: v,
                    new: v,
                }
            ), xs);
        };

        // until :: (a -> Bool) -> (a -> a) -> a -> a
        const until = (p, f, x) => {
            let v = x;
            while (!p(v)) v = f(v);
            return v;
        };

        // MARKDOWN AND RICH TEXT ATTRIBUTE RUNS ---------------------------------

        // mdEmphasisRuns :: String -> [{text::String, bold::Bool, italic::Bool}]
        const mdEmphasisRuns = s => {
            const ems = ['*', '_'];
            return snd(Boolean(length(s)) ? mapAccumL((a, [stars, txt]) => {
                const
                    mbStart = headMay(txt),
                    [q, r] = quotRem(stars.length, 2),
                    blnBold = q > 0 ? (
                        (!a.bold && (!mbStart.nothing && (mbStart.just !== ' ')))
                    ) : a.bold,
                    blnItalic = r > 0 ? (
                        (!a.italic && (!mbStart.nothing && (mbStart.just !== ' ')))
                    ) : a.italic;
                return [{
                        bold: blnBold,
                        italic: blnItalic
                    },
                    {
                        text: txt,
                        bold: blnBold,
                        italic: blnItalic
                    }
                ];
            }, {
                bold: false,
                italic: false
            }, splitEvery(2,
                append(elem(headMay(s)
                        .just, ems) ? [] : [''],
                    map(concat,
                        splitBy(
                            (a, b) =>
                            elem(a, ems) !== elem(b, ems),
                            s.split('')
                        )
                    )
                )
            )) : [{
                    bold: false,
                    italic: false
                },
                []
            ]);
        };

        // mdLinkRuns :: String -> [{ text::String, link::String }]
        const mdLinkRuns = s => {
            const dctParse = foldl((a, [lnk, lbl, url]) => {
                const parts = splitOn(lnk, a.rest);
                return length(parts) > 0 ? {
                    runs: a.runs.concat([{
                        text: parts[0]
                    }, {
                        text: lbl,
                        link: url
                    }]),
                    rest: parts[1]
                } : {
                    runs: a.runs,
                    rest: ''
                };
            }, {
                rest: s,
                runs: []
            }, allMatches(/\[([^\[]*)\]\(([^\)]*)\)/g, s));
            return append(dctParse.runs, {
                text: dctParse.rest
            });
        };

        // mdInlineRuns :: String ->
        //          [{text :: String, bold :: Bool, italic :: Bool, link: String}]
        const mdInlineRuns = s =>
            foldl((a, x) => append(
                    a,
                    isInfixOf('](', x.text) ? map(
                        dct => Object.assign(dct, {
                            bold: x.bold,
                            italic: x.italic
                        }),
                        mdLinkRuns(x.text)
                    ) : [x]
                ), [],
                mdEmphasisRuns(s)
            );

        // mdAppendedAsRT :: RichText -> String -> RichText
        const mdAppendedAsRT = (oRichText, strMD) =>
            foldl(newRunWithProps, oRichText, mdInlineRuns(strMD));



        // The function that follows is a very unpleasing hack ...
        // I haven't figured out how to do it properly ....

        // newRunWithProps :: RichText ->
        //          {text :: String, bold :: Bool, italic :: Bool, link :: String}
        //          -> RichText
        const newRunWithProps = (oRun, dct) => {
            const para = oRun.text()
                .length > 0 ? oRun.paragraphs.last : (
                    oRun.text = ' ',
                    oRun
                ),
                lastChar = para.characters.last,
                intSize = lastChar.size(),
                b = dct.bold,
                i = dct.italic;
            return (
                lastChar.duplicate(), // Hack:: .insert() seems to fail so:
                para.characters.last.size = intSize + 1, // Force a distinct attribute run
                (() => {
                    const newRun = Object.assign(
                            para.attributeRuns.last, {
                                text: dct.text,
                                font: 'HelveticaNeue' + ((b || i) ? '-' : '') +
                                    (b ? 'Bold' : '') + (i ? 'Italic' : ''),
                            }
                        ),
                        linkAttrib = newRun.style.attributes.byName('link');
                    return (
                        (linkAttrib
                            .value = (Boolean(dct.link) ? (
                                dct.link
                            ) : linkAttrib.defaultValue())),
                        newRun.size = intSize,
                        newRun
                    );
                })()
            );
        };

        // TEST ---------------------------------------------------------------------------

        // Select an OO5 row and run this to write the MD into the note as rich text.

        // A bit slow and hacky ...

        const strTest = 'Here **are** [some](http://www.apple.com) links [in the](http://www.google.com) **text *before** us*.';

        const
            oo = Application('OmniOutliner'),
            d = oo.documents.at(0),
            row = d.selectedRows.at(0),
            note = (row.noteExpanded = false, row.noteCell()),
            rtf = note.richText,
            delta = mdAppendedAsRT(rtf, strTest);

        return (
            row.noteExpanded = true,
            delta
        );

    })();