Still interested in Markdown support in OmniOutliner


#6

Here is my preferences:

Is Markdown just an import/export format, or an editing format?

Why not both? They’re not mutually exclusive. But I’d definitely see most utility in MD as editing formats in the text fields in the app itself.

Should OmniOutliner show the Markdown content, or the rendered content? Or Markdown while editing a cell, but rendered when you stop editing?

IMO the best way to go is a hybrid of doing both. The app Day One does this fantastically.

Should the outline structure follow the Markdown (e.g., when you type “###”)? Do you still see the “###” or does it just turn into indentation? What if the indentation is impossible to map to OmniOutliner’s levels (e.g. when you skip straight from “#” to “###” with no “##” in between)?

I was thinking that MD was just formatting language for text fields, not to structure the data of OmniOutliner.


#7

Note: iThoughts added Markdown support - editing and display - in both its Mac and iOS flavours.


#8

Huh. Nice response, Ken. I hadn’t considered all of the possible ways that Markdown Support could be interpreted.

As I end up with more and more places that accept Markdown, my use case would be simply exporting. I’d love to continue to use OmniOutliner for my editing and organizing, and then export ordered and unordered lists.

I’ve given up trying to export to Muggle-native formats and just give them a PDF of my outlines :)


#9
  • Should OmniOutliner support markdown? Yes, because there are so many situations, where markdown is a standard. Second rtf is not flexible and too strong connected to a layout.
  • Should OmniOutliner use markdown as a native format, speaking open and save it directly? No, because there are features in OmniOutliner, that could be transferred from and to markdown in different ways. That would be confusing and would lead to misuse the markdown format.
  • Should OmniOutline import and export markdown? For sure, because it is a future format and it should be possible.
  • Should OmniOutliner show the hash sings of the headlines? No, because the headline is part of a structure and could be used as a parent node.
  • A question, I have no answer to: How can we differ between headline nodes and list nodes? Maybe with a depth of headline option while importing and exporting. I know, that with that not every situation is covered.

By the way, the above was written in OmniOutliner 5. Just copied the nodes and pasted them here. Wonderful easy.


#10

I’d also love markdown support.
There are lots of ways to implement it, some are probably easier than others. For me, the great thing about markdown is its portability. I can start a a document in one program, then move back and forth between multiple programs working on that same document without having to convert/worry about compatibility issues.
I would love to have markdown implemented like OPML but with styling (or whatever styling markdown supports). Where I can edit and save markdown files completely in app.
Exporting/importing Markdown would also be welcome, though less idea.
As for editing in markdown, couldn’t that be a checkbox option. Check the box to allow markdown syntax. I think the syntax only needs to be visible on the element that’s currently being edited, but I don’t think any one implementation would be a deal breaker.
I think there are many ways to implement, and sure, you may not make everyone happy immediately, but I think introducing some markdown compatibility will be a step in the right direction, and you can always iterate/add features if users convince you of the need.
Thanks for this great app.


#11

Hi y’all. I’m working on implementing markdown export in javascript. I’ve made enough progress for what I have to be better than nothing, but there’s still a lot to do (and I’m not that good with javascript…). Help welcome! Here’s what I have so far:


#12

At a minimum, I would love to be able to export an outline in Markdown.

The developer of iThoughts, an amazing mindmap program, has a solution. You can make many decisions to get the text looking just how you want.

Right now, I need to

  1. export from OO to OPML.
  2. open in ithoughts
  3. export to markdown


#13

Here’s a script to convert the current OO document to markdown. (@steve, this will give you your minimum ask at least – pop this in your OO scripts folder and you’ll be able to export markdown from the script menubar with two clicks.)

Thanks to @SGIII for figuring out how to handle markdown conversion within rows, and to @draft8 for cleaning up some of the code. You guys are great. The script doesn’t support all markdown tags (e.g. code blocks, underlined text), but it does support headings/blockquotes/bold/italics/lists. Adding the rest should be fairly easy. NB not tested very thoroughly, though I ran a number of longer outlines through it and it worked as expected.

If you want conversion to tex/pdf, an extended version of the script that does that is here.

function run() {

// Setup

var app = Application.currentApplication();
app.includeStandardAdditions = true;
var OmniOutliner = Application('OmniOutliner');
var doc = OmniOutliner.documents[0];
var fileName = "outline.md";
var desktopString = app.pathTo("desktop").toString()

// The text of the document

var outlineText = "";

// Loop through rows and append their text to outlineText

doc.rows().forEach(function(theRow) {
	if (Object.keys(theRow.style.namedStyles).length > 0) {
		switch(theRow.style.namedStyles[0].name()) {
			case "Heading 1":
				outlineText += "# ";
				break;
			case "Heading 2":
				outlineText += "## ";
				break;
			case "Heading 3":
				outlineText += "### ";
				break;
			case "Blockquote":
				outlineText += "> ";
				break;
			case "Ordered List":
				outlineText += "1. ";
				break;
			case "Unordered List":
				outlineText += "* ";
				break;
		}	
	}
	outlineText += rowTextMD(theRow);
	outlineText += "\r";
});

// Convert the text of the paper to UTF8 encoding

outlineText = $.NSString.alloc.initWithUTF8String(outlineText);

// Write outlineText to a new markdown file

var file = `${desktopString}/${fileName}.md`
outlineText.writeToFileAtomicallyEncodingError(file, true, $.NSUTF8StringEncoding, null);

return true;

}

// From apple's documentation for Javascript for Automation
 
function writeTextToFile(text, file, overwriteExistingContent) {
    try {
 
        // Convert the file to a string
        var fileString = file.toString()
 
        // Open the file for writing
        var openedFile = app.openForAccess(Path(fileString), { writePermission: true })
 
        // Clear the file if content should be overwritten
        if (overwriteExistingContent) {
            app.setEof(openedFile, { to: 0 })
        }
 
        // Write the new content to the file
        app.write(text, { to: openedFile, startingAt: app.getEof(openedFile) })
 
        // Close the file
        app.closeAccess(openedFile)
 
        // Return a boolean indicating that writing was successful
        return true
    }
    catch(error) {
 
        try {
            // Close the file
            app.closeAccess(file)
        }
        catch(error) {
            // Report the error is closing failed
            console.log(`Couldn't close file: ${error}`)
        }
 
        // Return a boolean indicating that writing was successful
        return false
    }
}

// Code below written by draft8, based on code written by SGIII, in turn adapted from AppleScript code written by Rob Trew

const rowTextMD = row => {
        const
            as = row.topic.attributeRuns;
        return enumFromTo(0, as.length - 1)
            .reduce((s, i) => {
                const
                    attrib = as.at(i),
                    fnt = attrib.font(),
                    bld = (fnt.includes('Bold') || fnt.includes('Black')) ? (
                        '**'
                    ) : '',
                    ital = fnt.includes('Italic') ? '*' : '';
                return s + bld + ital + attrib.text() + ital + bld;
            }, '') + '\n';
    };
	
// enumFromTo :: Int -> Int -> [Int]
const enumFromTo = (m, n) =>
	Array.from({
	length: Math.floor(n - m) + 1
    }, (_, i) => m + i);

#14

@dmgrant, I’m grateful for the response! What kind of script is this? It doesn’t look like an applescript. I tried to save it using script editor, but it didn’t work. If you have a chance, could you tell me how to use it.


#15

Hi Steve! It’s javascript – apple added javascript scripting (called “Javascript for Automation” or JXA) recently. It’s significantly more powerful than applescript (and less painful to use IMO). To use the script you’ll just need to set the script type to “javascript” instead of “applescript” in a pulldown menu at the top of the script document.


#16

See a draft (Javascript for Automation) script using the same options as iThoughts at:


#17

Thank you. I didn’t know about the javascript automation. Unfortunately, I’m getting a syntax error. Another reason to bake this into OmniOutliner!


#18

I wonder if you are running a pre-Sierra version of macOS ?

That draft of the script is in ES6 JavaScript, and earlier version of macOS only support a mainly ES5 JS.

Generally you can get an ES5 version of an ES6 script by pasting it into the Babel JS REPL at https://babeljs.io

Here, for you to test, is an ES5 version of that script, as generated by Babel, and additionally wrapped in some lines that place a copy of the output in the clipboard:

var strClip = function () {
    'use strict';

    // OmniOutliner to TEXT-NEST, then TEXT-NEST to MARKDOWN

    // example of format translation through a textNest hasSubTopics

    // Rough draft ver 0.05  (ES5 translation, from Babel JS REPL)
    // 0.05 -- Moved extra linefeed from after hash header to before
    // 0.04 -- Simplified ooRowsJSO
    // 0.03 -- Slightly faster version of cellTextMD – fewer AE events

    // Copyright(c) 2017 Rob Trew
    //
    // Permission is hereby granted, free of charge, to any person obtaining a
    // copy of this software and associated documentation files(the "Software"),
    // to deal in the Software without restriction, including without limitation the rights
    // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
    // copies of the Software, and to permit persons to whom the Software is
    // furnished to do so, subject to the following conditions:
    //
    // The above copyright notice and this permission notice shall be included in all
    // copies or substantial portions of the Software.
    //
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    // SOFTWARE.

    // GENERIC ----------------------------------------------------------------

    // any :: (a -> Bool) -> [a] -> Bool

    var any = function any(f, xs) {
        return xs.some(f);
    };

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

    // curry :: Function -> Function
    var curry = function curry(f) {
        for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
            args[_key - 1] = arguments[_key];
        }

        var go = function go(xs) {
            return xs.length >= f.length ? f.apply(null, xs) : function () {
                return go(xs.concat(Array.from(arguments)));
            };
        };
        return go([].slice.call(args, 1));
    };

    // flip :: (a -> b -> c) -> b -> a -> c
    var flip = function flip(f) {
        return function (a, b) {
            return f.apply(null, [b, a]);
        };
    };

    // isInfixOf :: Eq a => [a] -> [a] -> Bool
    var isInfixOf = function isInfixOf(needle, haystack) {
        return haystack.includes(needle);
    };

    // log :: a -> IO ()
    var log = function log() {
        for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
            args[_key2] = arguments[_key2];
        }

        return console.log(args.map(show).join(' -> '));
    };

    // min :: Ord a => a -> a -> a
    var min = function min(a, b) {
        return b < a ? b : a;
    };

    // replicate :: Int -> a -> [a]
    var replicate = function replicate(n, a) {
        var v = [a],
            o = [];
        if (n < 1) return o;
        while (n > 1) {
            if (n & 1) o = o.concat(v);
            n >>= 1;
            v = v.concat(v);
        }
        return o.concat(v);
    };

    // replicateS :: Int -> String -> String
    var replicateS = function replicateS(n, s) {
        return concat(replicate(n, s));
    };

    // show :: a -> String
    var show = function show(x) {
        return JSON.stringify(x, null, 2);
    };

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    var zipWith = function zipWith(f, xs, ys) {
        return Array.from({
            length: min(xs.length, ys.length)
        }, function (_, i) {
            return f(xs[i], ys[i]);
        });
    };

    // JSO TEXT-NEST TO MARKDOWN ----------------------------------------------

    // jsoMarkdown :: Dictionary -> [Node] -> String
    var jsoMarkdown = function jsoMarkdown(dctOptions, xs) {
        var indent = dctOptions.indent || '\t',
            headerLevels = dctOptions.headerLevels || 3,
            listMarker = dctOptions.listMarker || '-',
            notesIndented = dctOptions.notesIndented || true;
        var jsoNodeMD = curry(function (intLevel, strIndent, node) {
            var blnHash = intLevel <= headerLevels,
                index = node.number,
                strNum = blnHash || index === undefined ? '' : index + '.',
                strPrefix = (blnHash ? replicateS(intLevel, '#') : strNum || listMarker) + ' ',
                noteIndent = notesIndented ? strIndent + indent : strIndent,
                nextIndent = blnHash ? '' : indent + strIndent,
                note = node.note,
                strNotes = note !== '' ? note.split('\n').map(function (x) {
                return noteIndent + x;
            }).join('\n') + '\n\n' : '',
                nest = node.nest;
            return (blnHash ? '\n' : '') + strIndent + strPrefix + node.text + '\n' + strNotes + (nest.length > 0 ? nest.map(jsoNodeMD(intLevel + 1, nextIndent)).join('') + '\n' : '');
        });

        return xs.reduce(function (a, x) {
            return a + jsoNodeMD(1, '', x) + '\n';
        }, '');
    };

    // OMNI-OUTLINER TO JSO / JSON --------------------------------------------

    // Either with or without (see blnMD) MarkDown emphases for Bold/Italic
    // ooRowsJSO :: Bool -> OO.Document -> [Node {text: String, nest: [Node]]}
    var ooDocJSO = function ooDocJSO(blnMD, doc) {
        return [{
            text: '[' + doc.name() + '](file:://' + Path(doc.file()).toString() + ')',
            nest: ooRowsJSO(blnMD, doc.children)
        }];
    };

    // ooRowsJSO :: Bool -> OO.Rows -> Node {text: String, nest: [Node]}
    var ooRowsJSO = function ooRowsJSO(blnMD, rows) {
        return rows().map(function (r, i) {
            return {
                text: blnMD ? cellTextMD(r.topicCell) : r.topic(),
                number: r.style.attributes.byName('heading-type(com.omnigroup.OmniOutliner)').value() !== 'None' ? i + 1 : undefined,
                note: blnMD ? cellTextMD(r.noteCell) : r.note(),
                nest: r.hasSubTopics ? ooRowsJSO(blnMD, r.children) : []
            };
        });
    };

    // contains :: String -> String -> Bool
    var contains = curry(flip(isInfixOf));

    // cellTextMD :: OO.Cell -> String
    var cellTextMD = function cellTextMD(cell) {
        var as = cell.richText.attributeRuns;
        return zipWith(function (txt, fnt) {
            var bld = any(contains(fnt), ['Bold', 'Black']) ? '**' : '',
                ital = isInfixOf('Italic', fnt) ? '*' : '';
            return bld + ital + txt + ital + bld;
        }, as.text(), as.font()).join('');
    };

    // TEST -------------------------------------------------------------------
    var ds = Application('OmniOutliner').documents,
        d = ds.length > 0 ? ds.at(0) : undefined;

    // Edit optional values for indent, headerLevels, notesIndented, listMarker
    return d ? jsoMarkdown({
        indent: replicateS(4, ' '), // or '\t'
        headerLevels: 3,
        notesIndented: true,
        listMarker: '-' // or '*' or '+'
    }, ooDocJSO(true, d)[0].nest) : '';
}();

var a = Application.currentApplication(),
    sa = (a.includeStandardAdditions = true, a);

sa.setTheClipboardTo(strClip);

strClip;

#19

If you use Hazel, I have a simple workaround for you.

You will need to install a utility called Pandoc, details of what Pandoc is, how it works and how to install can be found here -> pandoc.org

  1. Install Pandoc
  2. Create a folder for Hazel to watch
  3. Create the follow rule on the folder in Hazel

  1. Click edit script and add the script (text below)

pandoc "$1" -t markdown -o "$1"
  1. Save the file you want to convert to Markdown in the folder as an OPML file. Hazel will run the script and convert the file to markdown for you.

#20

Thanks so much deano1406! This works beautifully.


#21

Of course, I’d still like to see OO export markdown without such help.


#22

I’m sure Markdown support will appear at some point, when I’m working on documents I use OmniOutliner. But when I’m exchanging files with colleagues or keeping them for long term Archive I use Markdown.

Import and Export into OmniOutliner would be top of my list, followed by Markdown support within OmniOutlier a secondary consideration.


#23

I need this feature NOW.
I’ve been creating weekly reports in OmniOutliner… but I have no way to copy and paste the report into an email.,. at least none that i’ve found.

I mean, sure, I can get a simple “text” format, but then I have to go and painfully reformat the darn thing… I’m going to have to write a python script to do this form me!!

Is there any sort of “plugin” capability for OO that I could use to add a “Copy as Formatted…”?


#24

@kutenai Could you explain what reformatting you need to do, please? My guess would be removing the handles and/or checkboxes. If you don’t want any, you can control this by clearing the fields in the Text or RTF Export tabs of the Preferences. You can even change indentation values if you want something different.


#25

The output indentation and formatting does not translate well to my mail client – MailMate – which accepts a nice markdown format. I just want the Header rows to have a ‘#’ as prefix, and then indent the child rows.

You pointed out the configuration options, and that will help actually. I can make the ‘header rows’ have a # in front of them. Would be nice to have some additional controls, but this help.

As a side-note, I used bbedit and a custom text filter to re-format to clean Markdown.