Paste tables as MultiMarkdown in Editorial for iPad

I purchased Editorial when it first came out last year, and I’ve used it to write a number of documents including several posts for this blog. Its Markdown support is great, and whatever dialect it uses aligns well with the features I need from MultiMarkdown and kramdown, the two variants of the language that I use.1

The main feature of Editorial—the one that set it apart from every other Dropbox-enabled Markdown editor—is its extensive automation and scriptability support in the form of workflows. I have never been big on scripting my editors, and so while I spent some time exploring this feature when I first purchased the app I didn’t do much with it. I wrote a simple script to convert pasted links, imported a couple other Markdown link scripts, and not much else. Not much else was needed; I mostly write text and when I need something more complex I do it on the computer.

But I do make tables. Most of my brewing posts and my fitness progress posts contain simple tables written in the MultiMarkdown table format, as so:

| Header | Row     | Goes | Here     |
|:-------|:--------|:-----|:---------|
| Then   | we      | have | all      |
| the    | content | of   | the      |
| table  | like    | this | example. |

But aligning everything by hand feels like more work than necessary. It’s so much easier to type this:

| Header | Row | Goes | Here |
|--|--|--|--|
| Then | we | have | all |
| the | content | of | the |
| table | like | this | example. |

Both convert to the same output (other than the cell alignment, which is specified in the first example) as shown.

Header Row Goes Here
Then we have all
the content of the
table like this example.

So why does it matter? Because I want my source files to be just another version of my content. They should be human readable. As John Gruber, the creator of Markdown, puts it:

The overriding design goal for Markdown’s formatting syntax is to make it as readable as possible. The idea is that a Markdown-formatted document should be publishable as-is, as plain text, without looking like it’s been marked up with tags or formatting instructions.

A nicely aligned table is easy to read and understand.

This was a perfect chance to use Editorial’s workflows and python scripting. I could get software to do boring work for me and just let me write. I already had Dr. Drang’s table scripts for BBEdit installed, and the formatting script was written in python. It was simple to convert this to a filter script for Editorial instead:

#coding: utf-8
import workflow
"""Prettify MultiMarkdown Table

Formats a MultiMarkdown table into a neat set of columns

This is basically just a minor edit of Dr. Drang's script, converted
to an Editorial python workflow.

http://www.leancrew.com/all-this/2012/11/markdown-table-scripts-for-bbedit/
"""

unformatted = workflow.get_input()

def just(string, type, n):
    "Justify a string to length n according to type."

    if type == '::':
        return string.center(n)
    elif type == '-:':
        return string.rjust(n)
    elif type == ':-':
        return string.ljust(n)
    else:
        return string


def normtable(text):
    "Aligns the vertical bars in a text table."

    # Start by turning the text into a list of lines.
    lines = text.splitlines()
    rows = len(lines)

    formatrow = 1

    # Figure out the cell formatting.
    # First, find the separator line.
    for i in range(rows):
        if set(lines[i]).issubset('|:.-'):
            formatline = lines[i]
            formatrow = i
            break

    # Delete the separator line from the content.
    del lines[formatrow]

    # Determine how each column is to be justified.
    formatline = formatline.strip(' ')
    if formatline[0] == '|': formatline = formatline[1:]
    if formatline[-1] == '|': formatline = formatline[:-1]
    fstrings = formatline.split('|')
    justify = []
    for cell in fstrings:
        ends = cell[0] + cell[-1]
        if ends == '::':
            justify.append('::')
        elif ends == '-:':
            justify.append('-:')
        else:
            justify.append(':-')

    # Assume the number of columns in the separator line is the number
    # for the entire table.
    columns = len(justify)

    # Extract the content into a matrix.
    content = []
    for line in lines:
        line = line.strip(' ')
        if line[0] == '|': line = line[1:]
        if line[-1] == '|': line = line[:-1]
        cells = line.split('|')
        # Put exactly one space at each end as "bumpers."
        linecontent = [ ' ' + x.strip() + ' ' for x in cells ]
        content.append(linecontent)

    # Append cells to rows that don't have enough.
    rows = len(content)
    for i in range(rows):
        while len(content[i]) < columns:
            content[i].append('')

    # Get the width of the content in each column. The minimum width will
    # be 2, because that's the shortest length of a formatting string and
    # because that matches an empty column with "bumper" spaces.
    widths = [2] * columns
    for row in content:
        for i in range(columns):
            widths[i] = max(len(row[i]), widths[i])

    # Add whitespace to make all the columns the same width and 
    formatted = []
    for row in content:
        formatted.append('|' + '|'.join([ just(s, t, n) for (s, t, n) in zip(row, justify, widths) ]) + '|')

    # Recreate the format line with the appropriate column widths.
    formatline = '|' + '|'.join([ s[0] + '-'*(n-2) + s[-1] for (s, n) in zip(justify, widths) ]) + '|'

    # Insert the formatline back into the table.
    formatted.insert(formatrow, formatline)

    # Return the formatted table.
    return '\n'.join(formatted)

# Return formatted output to workflow. 
formatted = normtable(unformatted)
workflow.set_output(formatted)

99% of this is a simple copy and paste of Dr. Drang’s work. I added this script to an Editorial Workflow I call Format Table. It takes selected text (such as the messy table shown above) and makes it pretty using Dr. Drang’s script.

In addition to making handmade tables pretty, I also occasionally paste tables from Pages, Numbers, or web sites that I want to use. These generally get pasted as tab separated tables. Dr. Drang provided a short script in perl that converts them to basic MultiMarkdown, but I had to write something in python for Editorial. What I put together isn’t fantastic, but it works:

#coding: utf-8
import workflow
"""Convert to MultiMarkdown table

Python script for Editorial that converts copied tables from the web, 
spreadsheets, and so on (anything that hits the clipboard as a tab
separated table) to a basic MultiMarkdown table."""

input_table = workflow.get_input()

# Split into a list of lines, assume first line is the header row.
lines = input_table.splitlines()
first_line = lines.pop(0)

# Count tabs to get columns. There is one more column than tabs.
columns = first_line.count('\t') + 1

# Build a basic format line as a list. Will output a format string
# in the form of '---|----|----|----|---'
formatline = []
for col in range(0, columns):
    formatline.append('--')
formatstring = '-|-'.join(formatline)

# Replace tabs in each line of the original table with |, and build a list
content = []
content.append(first_line.replace('\t',' | '))      # Header row
content.append(formatstring)                        # Separator row
for line in lines:                                  # Table content
    content.append(line.replace('\t',' | '))

# Add leading and trailing | characters
formatted = []
for row in content:
    formatted.append('|' + row + '|')

# Return the markdown table as a string separated by newlines
table = '\n'.join(formatted)

workflow.set_output(table)

I made a second workflow for Editorial called Paste as Table that combines this with the first script, and applies it to the contents of the clipboard. If you copy a table from another app like Numbers or Safari, and use this workflow, the table will be pasted as nicely formatted MultiMarkdown at the cursor position in your Editorial document.

I put both of these workflows in my toolbar and now if I copy a table from the web or create one in Numbers it will paste cleanly and quickly right into my document. I’m sure this has already been done by others, and I’m sure I spent more time on this than I needed to, and I’m sure this script does not handle edge cases gracefully, but it does work rather well for me. Hopefully someone else gets some use out of this too.

— Steve

  1. There are several variants of Markdown that add a number of features to the original implementation. The key features that I use are tables and footnotes.

Posted on 17 May 2014