Request: Copy Selected Rows in Markdown

Continuing the discussion from Using It For Blogging:

Does anyone have a script to copy selected rows in markdown? Or, it might simply work to always copy in plain text instead of rich text.

If the OO outline is kept in plain text, then it copies in a markdown ready format. This is determined from my preferences
47%20PM

  • Introduction
    • Idea 1 Completed
    • Idea 2

However, if I have any rich text, it will pick up the preferences for rich text export.

53%20PM

Introduction

• Idea 1 Completed

• Idea 2

1 Like

I created one that does this. In OmniOutliner, I created some styles to indicate different markdown styles. Each style has a different color fo easy visual indication, and I’ve mapped each to a function key for easy assignment. My conversion script only works on the first style found, so if you assign an OmniOutliner row both “Ordered List” and “Code”, it’ll only take the first style. (I have no idea which one the script will grab, lol)

I created the following styles:

  1. F1 – Ordered List
  2. F2 – Unordered List
  3. F3 – Paragraph
  4. F4 – Blockquote
  5. F5 – Code

The code is below; sorry about the strange discourse styling, but all of the below are part of the script.

It spits it out into Drafts 5, which is available on iOS and macOS for free, so I just copy and paste my Markdown out of there.

/*{
 "type": "action",
 "targets": ["omnioutliner"],
 "author": "Macdork, based on work by Marc A. Kastner",
 "description": "Create Markdown & export it to Drafts",
 "label": "Markdown to Drafts",
 "paletteLabel": "Drafts Doc"
}*/

function msg(msg) {
    console.log(msg)
}

function checkCodeBlockSiblings(item) {
    /* use item.followingSiblings and item.precedingSiblings to get
    arrays of other siblings to check for more code blocks before assigning
    pre and post markdown.  Returns array of pre and post markdown */
    var newline = '\n'
    var pre = ''
    var post = ''
    var precededByCode = false
    var followedByCode = false
    
    msg("--------------\nProcessing " + item.topic)
    var precedingArray = item.precedingSiblings
    if (typeof precedingArray != 'undefined' && precedingArray instanceof Array) {
        // we know it has a sibling
        var precItem = precedingArray[precedingArray.length - 1]
        if (precItem != null) {
            // probably redundant
            msg("preceding topic:  " + precItem.topic)
            if (typeof (precItem.style.namedStyles[0]) != 'undefined') {
                // this item is styled; let's see what it is
                precStyle = precItem.style.namedStyles[0].name
                if (precStyle == "Code") {
                    precededByCode = true
                    msg("prec was code, so precededByCode =  " + precededByCode)
                }
            } // end of block that checked for named styles
        } // possibly redundant null check
    }
    if (precededByCode) {
        // preceded by code block, so don't add more markdown
        pre = ''
    } else {
        // wasn't preceded by a code block, so add the markdown
        pre = newline + "```" + newline
    }
    
    // repeat the process for the post markdown
    var followingArray = item.followingSiblings
    if (typeof followingArray != 'undefined' && followingArray instanceof Array) {
        var folItem = followingArray[0]
        if (folItem != null) {
            if (typeof (folItem.style.namedStyles[0]) != 'undefined') {
                // this item is styled; let's see what it is
                folStyle = folItem.style.namedStyles[0].name
                if (folStyle == "Code") {
                    followedByCode = true
                    msg("following is code, so followedByCode =  " + followedByCode)
                } 
            }
        }
    }
    if (followedByCode) {
        // followed by code block, so don't add more markdown
        post = ''
    } else {
        // not followed by a code block, so add the markdown
        post = newline + "```" + newline
    }
    
    return [pre, post]
}

function getMarkdown(item) {
    /* 
    Given an item, return value array with all the mardown needed, and
    info needed to determine the level of indention
    */
    var isList = false
    var pre = ''
    var post = ''
    var numPounds = item.level + 1 // +1 since only the doc title should have 1
    var newline = '\n'
    
    // check the item to see if it's styled first
    if (typeof (item.style.namedStyles[0]) !== "undefined") {
        styleName = item.style.namedStyles[0].name // first named style
        switch(styleName) {
            case "Ordered List":
                pre = "1.  "
                isList = true
                break;
            case "Unordered List":
                pre = "* "
                isList = true
                break;
            case "Paragraph":
                pre = newline
                post = newline
                break;
            case "Blockquote":
                pre = "> "
                // post = newline
                break;
            case "Code":
                codeMarkdown = checkCodeBlockSiblings(item)
                pre = codeMarkdown[0]
                post = codeMarkdown[1]
                // pre = "```" + newline
                // post = newline + "```"
                break;
        } // end of switch statement
    } else {
        /* Handle un-styled item as a heading; level depth = num of #'s + 1.
        I'm adding one additional # to account for my feeling that the doc 
        title should have 1 #, and every other heading should be smaller than
        the title.
        */
        pre = newline + '#'.repeat(numPounds) + " "
        post = newline
    }    
    return [pre, post, isList]
}

function getNumTabs (item) {
    /* Given an item, returns the number of tabs to indent it
       Recursively calls itself on its parent to gather parent's info.  End
       condition is when we reach a parent whose number of tabs is 0.  
       Using documentation from this page: https://omni-automation.com/omnioutliner/item.html
       item.parent:  (Item or nil r/o) • Returns the item that contains this item, or null 
           if this is the root item.
    */
    var parent = item.parent       
    // check to be sure we haven't returned a null parent
    if (parent != null) {
        var parentMD = getMarkdown(parent)
        parentIsList = parentMD[2] // 
        // if parent's not a list, return 0; if it is, we need to go higher
        if (!parentIsList) {
            return 0
        } else if (parentIsList) {
            // parent was a list, so let's recurse higher
            return 1 + getNumTabs(parent)
        }
    } else {
        // we've reached the root item; return 0
        return 0
    }
}

var _ = function() {
    var action = new PlugIn.Action(function(selection, sender) {
        var topics = new Array()
        var tab = '\t'
        var newline = '\n'
        var noteString = ''
        var numTabs = 0
        var tabs = ''
        console.clear()
        
        // add document title
        docTitle = "# " + document.name
        topics.push(docTitle)

        // loop through the document's items
        rootItem.descendants.forEach(function(item) {
            // reset variables for this iteration
            var mdInfo = getMarkdown(item)
            var pre = mdInfo[0]
            var post = mdInfo[1]
            var isList = mdInfo[2]
            //msg("index:  " + item.index)

            // now find out how far to indent this item
            if (isList) {
                numTabs = getNumTabs(item)
                tabs = tab.repeat(numTabs)
            }
            
            // create the string w/ all its markdown
            mdTopic = tabs + pre + item.topic + post
            topics.push(mdTopic)

            // handle item notes as a paragraph
            if (item.note) {
                noteString = item.note + newline
                topics.push(noteString)
                noteString = ''            
            }
        })
        topics.join(newline) //convert the array to newline-separated string
        encodedStr = encodeURIComponent(topics.join(newline))
        urlStr = "drafts5://x-callback-url/create?text=" + encodedStr
        url = URL.fromString(urlStr)
        url.call(function(result) {
            console.log(result)
        })
    });

    action.validate = function(selection, sender) {
        if (rootItem.descendants.length > 0) {
            return true
        } else {
            return false
        }
    }

    return action
}();
_;
1 Like

Thanks for sharing your script, @macdork. I had been looking for something like this in QUITE a while.

Still needs a little work, but it’s close enough for my needs.

1 Like

@macdork, thanks for creating this! I didn’t notice the update. I’m surprised that this isn’t built into OmniGroup apps especially with the popularity of markdown.

1 Like

Cheers! Glad it’s useful :)

@macdork, could you point me in the direction of how I would copy the end result to the clipboard? (I’ve been trying without success as I know very little javascript). I will comment out the export to drafts part and paste it elsewhere instead.

FWIW, if you want to copy the sentence “Hello World” to the clipboard, this would do it:

Pasteboard.general.string = 'Hello World'

2 Likes

Perfect, thanks!

1 Like

Thanks for this!

1 Like

@unlocked2412
I’ve modified it to just copy to the clipboard, and added “Task” as a style now, too. Task prepends a line with 1. [ ] and treats it like list.

/*{
 "type": "action",
 "targets": ["omnioutliner"],
 "author": "MacDork, based on work by Marc A. Kastner",
 "description": "Create Markdown & copy it to the clipboard",
 "label": "Markdown to Clipboard",
 "paletteLabel": "MarkDown"
}*/

/* https://discourse.omnigroup.com/t/request-copy-selected-rows-in-markdown/44714/2
*/

function msg(msg) {
    console.log(msg)
}

function checkCodeBlockSiblings(item) {
    /* use item.followingSiblings and item.precedingSiblings to get
    arrays of other siblings to check for more code blocks before assigning
    pre and post markdown.  Returns array of pre and post markdown */
    var newline = '\n'
    var pre = ''
    var post = ''
    var precededByCode = false
    var followedByCode = false
    
    msg("--------------\nProcessing " + item.topic)
    var precedingArray = item.precedingSiblings
    if (typeof precedingArray != 'undefined' && precedingArray instanceof Array) {
        // we know it has a sibling
        var precItem = precedingArray[precedingArray.length - 1]
        if (precItem != null) {
            // probably redundant
            msg("preceding topic:  " + precItem.topic)
            if (typeof (precItem.style.namedStyles[0]) != 'undefined') {
                // this item is styled; let's see what it is
                precStyle = precItem.style.namedStyles[0].name
                if (precStyle == "Code") {
                    precededByCode = true
                    msg("prec was code, so precededByCode =  " + precededByCode)
                }
            } // end of block that checked for named styles
        } // possibly redundant null check
    }
    if (precededByCode) {
        // preceded by code block, so don't add more markdown
        pre = ''
    } else {
        // wasn't preceded by a code block, so add the markdown
        pre = newline + "```" + newline
    }
    
    // repeat the process for the post markdown
    var followingArray = item.followingSiblings
    if (typeof followingArray != 'undefined' && followingArray instanceof Array) {
        var folItem = followingArray[0]
        if (folItem != null) {
            if (typeof (folItem.style.namedStyles[0]) != 'undefined') {
                // this item is styled; let's see what it is
                folStyle = folItem.style.namedStyles[0].name
                if (folStyle == "Code") {
                    followedByCode = true
                    msg("following is code, so followedByCode =  " + followedByCode)
                } 
            }
        }
    }
    if (followedByCode) {
        // followed by code block, so don't add more markdown
        post = ''
    } else {
        // not followed by a code block, so add the markdown
        post = newline + "```" + newline
    }
    
    return [pre, post]
}

function getMarkdown(item) {
    /* 
    Given an item, return value array with all the mardown needed, and
    info needed to determine the level of indention
    */
    var isList = false
    var pre = ''
    var post = ''
    var numPounds = item.level + 1 // +1 since only the doc title should have 1
    var newline = '\n'
    
    // check the item to see if it's styled first
    if (typeof (item.style.namedStyles[0]) !== "undefined") {
        styleName = item.style.namedStyles[0].name // first named style
        switch(styleName) {
            case "Ordered List":
                pre = "1.  "
                isList = true
                break;
            case "Unordered List":
                pre = "* "
                isList = true
                break;
            case "Task":
                pre = "1. [ ] "
                isList = true
                break;
            case "Paragraph":
                pre = newline
                post = newline
                break;
            case "Blockquote":
                pre = "> "
                // post = newline
                break;
            case "Code":
                codeMarkdown = checkCodeBlockSiblings(item)
                pre = codeMarkdown[0]
                post = codeMarkdown[1]
                // pre = "```" + newline
                // post = newline + "```"
                break;
        } // end of switch statement
    } else {
        /* Handle un-styled item as a heading; level depth = num of #'s + 1.
        I'm adding one additional # to account for my feeling that the doc 
        title should have 1 #, and every other heading should be smaller than
        the title.
        */
        pre = newline + '#'.repeat(numPounds) + " "
        post = newline
    }    
    return [pre, post, isList]
}

function getNumTabs (item) {
    /* Given an item, returns the number of tabs to indent it
       Recursively calls itself on its parent to gather parent's info.  End
       condition is when we reach a parent whose number of tabs is 0.  
       Using documentation from this page: https://omni-automation.com/omnioutliner/item.html
       item.parent:  (Item or nil r/o) • Returns the item that contains this item, or null if this is the root item.
    */
    var parent = item.parent       
    // check to be sure we haven't returned a null parent
    if (parent != null) {
        var parentMD = getMarkdown(parent)
        parentIsList = parentMD[2] // 
        // if parent's not a list, return 0; if it is, we need to go higher
        if (!parentIsList) {
            return 0
        } else if (parentIsList) {
            // parent was a list, so let's recurse higher
            return 1 + getNumTabs(parent)
        }
    } else {
        // we've reached the root item; return 0
        return 0
    }
}

var _ = function() {
    var action = new PlugIn.Action(function(selection, sender) {
        var topics = new Array()
        var tab = '\t'
        var newline = '\n'
        var noteString = ''
        var numTabs = 0
        var tabs = ''
        console.clear()
        
        // add document title
        docTitle = "# " + document.name
        topics.push(docTitle)

        // loop through the document's items
        rootItem.descendants.forEach(function(item) {
            // reset variables for this iteration
            var mdInfo = getMarkdown(item)
            var pre = mdInfo[0]
            var post = mdInfo[1]
            var isList = mdInfo[2]
            //msg("index:  " + item.index)

            // now find out how far to indent this item
            if (isList) {
                numTabs = getNumTabs(item)
                tabs = tab.repeat(numTabs)
            }
            
            // create the string w/ all its markdown
            mdTopic = tabs + pre + item.topic + post
            topics.push(mdTopic)

            // handle item notes as a paragraph
            if (item.note) {
                noteString = item.note + newline
                topics.push(noteString)
                noteString = ''            
            }
        })
        mdText = topics.join(newline) //convert array to string
        Pasteboard.general.string = mdText
    });

    action.validate = function(selection, sender) {
        if (rootItem.descendants.length > 0) {
            return true
        } else {
            return false
        }
    }

    return action
}();
_;
3 Likes

Hello, Sorry to be a dingbat. I can’t see how to get this script to do anything. I made a simple outline (3 rows, one column). I saved your script above to a quotes.omnijs file in my plugins. I see it listed in the Automation menu (now that I have updated to the 5.3.x). I select the rows, and select quotes from the Automation choices, but nothing is my clipboard. What am I doing wrong? Thanks!

I’ll try to take a look tomorrow and see if i can assist.

Well… I know this is totally not helpful, but I created a new plugin using the code copy-pasted from this page and it works,

:(

I know at least at this point that the code is still good, and nothing broke in recent versions of OmniOutliner, so I’m not sure where to go from here, except to say that the configuration might be the next place to look.

I’ve not got a lot of time right now, but I’ll try again tomorrow to document what I did today to test it and maybe we can narrow down the issue some more then.

Thanks so much for your efforts. When I click Plug-ins it just pulls up the folder, in Finder. There’s not “select which plugins to use” and the ones in the folder (viewed in Finde) appear in the Automation Menu as grayed out-unavailable options. I sent a note to Omni about it. I just upgraded to get this Automation Menu but what i see doesn’t look like the documentation.

Well, mine’s not working now, either. So… Ugh. I’ll have to dig in a bit later and figure this out. I was just using it two days ago and it worked fine, but something’s changed =/

@macdork Would love to give this a whirl. I can’t get any of the versions in this thread to show up as actions in the Automation Menu. Were you able to figure out what has changed?

Not yet – I haven’t needed it lately 😅

I’ll try to have a look this week.

1 Like