Creating nice Markdown tables with Pythonista

There has been a lot of footnotes on the web lately and most of the times that wouldn't be viable without MultiMarkdown, an extended version of Markdown made by Fletcher Penney. One of the coolest things about MultiMarkdown is that it has a syntax to create tables, however, that's not as simple as you may think.

If you want to see the structure of a table in MultiMarkdown, check the documentation. In short terms, every row is wrapped in vertical bars (|) and cells are delimited by another vertical bar. The table needs a table header, quite important to define the alignment of cells and the number of columns.

Dr. Drang wrote about Markdown tables here and you should check it out if you got a itch in your head already. Jeff Mueller and Ben Tsai have some nice examples on how you can use Markdown tables to organize your day-to-day information.

I wanted to create a flexible method to create a table without the overwhelming syntax, I kept every row as a line, but I had to figure out a way to split the cells. You could think of many separators, but I stuck with two spaces because I can create everything from the topmost keyboard layer1. Considering this presupposition, this is how we would type a table:

First Header  Second Header  Third Header
1st Item  2nd Item  3rd Item
One cell  Two cells
Two cells   One cell

First, we gonna split our table in every line break, giving us a list of our rows, for every row, we gonna split it using re.split() by the spaces preceded by another space. Doing this will let us extend cells later. This generates a list of sublists:

ttable = [split('(?<=\s)\s', row) for row in table.split('\n')]
# [['First Header ', 'Second Header ', 'Third Header'], ['1st Item ', '2nd Item ', '3rd Item'], ['One cell ', 'Two cells'], ['Two cells ', '', 'One cell'], ['Awesome']]

Notice that since the beginning I didn't make any differentiation about what is the header, that's because I found it simpler to just assume it is the first row. So we must count the number of items in the header row, then we can also get the amount of vertical bars there, just by adding 1 to the the row's length.

columnCount = len(ttable[0])
# 3
barCount = columnCount + 1
# 4

Table separators in MultiMarkdown are actually quite simple: |:--| means the column is aligned to the left, |--:|, aligned to the right, |:--:|, centralized. We need a number of columns equal to the length of the header, so we gonna multiply one of those snippets by it:

headerSeparator = '|:--' * columnCount + '|'
# |:--|:--|:--|

You can customize the alignment, just remember it will affect the whole table. Notice how we also append a vertical bar to close the row.

Next, we gonna join each one of our cells with a vertical bar, but can you see that we left a blank white space after each cell? Look again at the outcome from the ttable variable if you missed that. Saw it now? We gotta strip that.

ttable2 = ['|' + '|'.join([cell.rstrip() for cell in row]) + '|' for row in ttable]
# ['|First Header|Second Header|Third Header|', '|1st Item|2nd Item|3rd Item|', '|One cell|Two cells|', '|Two cells||One cell|', '|Awesome|']

Now allow me to explain that list comprehension there. We're looping through our table and each item is called a row, which is also a list, so we loop into it and use rstrip() to remove all trailing white space on the end of the string, then we join these cells with vertical bars and the whole row into them as well.

Let's check our first row:

['First Header ', 'Second Header ', 'Third Header']

You know it is a list because it is wrapped in brackets, each item is separated by commas. Then we built another list comprehension for each cell within this row:

[cell.rstrip() for cell in row]
# ['First Header', 'Second Header', 'Third Header']

Then we join this outcome with vertical bars, turning it into a string:

'First Header|Second Header|Third Header'

Then we wrap in vertical bars:

'|First Header|Second Header|Third Header|'

This is what happens to every row in our list. If you add our headerSeparator now, the list will render poorly because we don't have the exact number of cells in each row to fill all the columns, so we create a loop to fix that and add more vertical bars as needed.

for row in enumerate(ttable2):
    rowCount = row[1].count('|')
    if rowCount < barCount:
        ttable2[row[0]]+='|' * (barCount - rowCount)

We'll use enumerate to create a tuple with the index of the row in our table, this means that our first row, the header, will be:

(0, '|First Header|Second Header|Third Header|')

Then we count the amount of vertical bars in each row and if that value is less than the amount of our header, which we calculated previously, then it appends more vertical bars to the row, extending the last cell to fill the remaining columns2.

Also, if you didn't tested yet, you can extend a previous cell to cover more columns by adding an extra white space, that's how we get our Two cells cell in the fourth row to cover two columns.

Time has come to insert our separator and join our list with line breaks:

ttable2.insert(1, headerSeparator)
print '\n'.join(ttable2)
# |First Header|Second Header|Third Header|
# |:--|:--|:--|
# |1st Item|2nd Item|3rd Item|
# |One cell|Two cells||
# |Two cells||One cell|
# |Awesome|||
First Header Second Header Third Header
1st Item 2nd Item 3rd Item
One cell Two cells
Two cells One cell

If you're attached to your double spaces shortcut in iOS, I also created a version using commas. To have a comma as part of the cell content, precede it with a backslash (). To extend a cell, add a comma. Just replace the ttable variable for this:

ttable = [[re.sub('\\\,',',',cell) for cell in re.split('(?<!\\\),', row)] for row in table.split('\n')]

The coolest thing about using commas instead of double spaces is that by tweaking the action a little you can easily create empty cells in the end of the row. Just replace our ttable2 variable:

ttable2 = ['|' + '|'.join([cell.rstrip() if cell != ' ' else cell for cell in row]) + '|' for row in ttable]

Then cells with only an empty space will be counted. This is an example:

First Header,Second Header,Third Header
1st Item,2nd\,Item,3rd Item
,One cell,One cell
One cell,One cell,

|First Header|Second Header|Third Header|
|1st Item|2nd,Item|3rd Item|
||One cell|One cell|
|One cell|One cell| |

Which will generate this table:

First Header Second Header Third Header
1st Item 2nd,Item 3rd Item
One cell One cell
One cell One cell

You can get both versions on GitHub. To use it in your iOS device, import the sys module and replace our table variable for table = sys.argv[1]. Also import the urllib and webbrowser modules. Remove our print from the end of the script and add the following to send the table to Drafts:

callback = 'drafts://x-callback-url/create?text=%s' % urllib.quote('\n'.join(ttable2))

I included a version using the comma syntax to the same Gist so you can get a reference. You can call it from Drafts using the following action:

Convert to Table:


You really should get Pythonista right now.

  1. By default, iOS comes with a text replacement linked to double spaces, which converts it do period + space. To disable this options, go to Settings > General > Keyboard > turn off the "." Shortcut option. 

  2. I tested the outcome in many apps, however, only Byword properly rendered the extended cells.