# Converting Pandoc Markdown images from captioned to inline

After writing a rather long document in Markdown and using pandoc to convert it to a PDF, I found, to my dismay, that many of the images were out of place, and that they all had their alternate text proudly displayed underneath them as captions. My document is rather instructional, so this rearrangement was harmful to its readability.

I eventually found a way to display the images as inline. I still wanted to write the document in standard Markdown, though, so I wrote a Python script to convert all the standalone images in a document to this inline form.

pandoc_images.py:

import sys

# Convert standalone images in standard Markdown
# to inline images in Pandoc's Markdown
with open(sys.argv[1], 'r') as markdown:
for index, line in enumerate(lines):
is_first_line = index == 0
preceding_blank = True if is_first_line else not lines[index - 1]

is_last_line = index == len(lines) - 1
following_blank = True if is_last_line else not lines[index + 1]

is_standalone = preceding_blank and following_blank
is_image = line.startswith('![') and '](' in line and line.endswith(')')
print(line + ('\\\n' if is_standalone and is_image else ''))


Example (text.md):

This is some text.

![This is an image.](image.png)



Running python3 pandoc_images.py text.md would produce:

This is some text.

![This is an image.](image.png)\



It seems like a lot of mess (enumerate, bounds checking, etc.) for such a simple job, though. Is there any way I can improve any of this code?

• Could you add an example of a standalone image and how it is converted to an inline image? – mkrieger1 Aug 31 '15 at 15:31
• @mkrieger1 Sure, I'll do that as soon as I get the chance. – Sam Estep Aug 31 '15 at 15:38

Instead of a ternary, use the or syntax for setting your booleans here. If is_first_line is True then True is returned, if it's not then not lines[index - 1] is evaluated and the result of that is returned whether it's True or False.

preceding_blank = is_first_line or not lines[index - 1]


But since you're setting is_first_line one line before and never using it again, I'd even fold that into this expression.

preceding_blank = index == 0 or not lines[index - 1]


I'd make both the same changes with is_last_line. Also I would substitute index for i, since i is idiomatic for index anyway and will save some characters.

One last thing that you may want, adding .strip() to not line will strip out any whitespace on the line and make sure that even an empty whitespace line will be considered False because any character in a string will make it evaluate as True. This may or may not be beneficial to you, as whitespace could be meaningful.

with open(sys.argv[1], 'r') as markdown:
for i, line in enumerate(lines):
precedingBlank = i == 0 or not lines[i - 1].strip()
followingBlank = i == len(lines) - 1 or not lines[i + 1].strip()
is_standalone = preceding_blank and following_blank

is_image = line.startswith('![') and '](' in line and line.endswith(')')
print(line + ('\\\n' if is_standalone and is_image else ''))

• Ah, I had forgotten that Python uses short-circuiting in boolean expressions, thanks. Are you sure the name should be isFirstLine instead of is_first_line? PEP 8 seems to say otherwise. Finally, since trailing whitespace is significant in standard Markdown, I'm worried that using strip will cause problems in the future. Any further comment on that? – Sam Estep Aug 31 '15 at 12:06
• @Elogent Apologies! I was apparently entirely wrong about naming, not sure why I thought that and thanks for correcting me. I'm not familiar with Markdown so I didn't want to imply that you definitely should use strip, it's just commonly used in cases like this and I was suggesting it. I've edited my answer to be a bit clearer that it might be useful. – SuperBiasedMan Aug 31 '15 at 13:20

def convert(s):
return re.sub(r"((?:\A|^ *\n)!$.*$$$.*$$)\n(^ *\n|\Z)", r"\1\\\2", s, 0, re.M)

def test1():
print convert("""![foo](bar)\n\nthis is a test\n""")

def test2():
print convert("""line 1\n\n![foo](asd)\n\nanother test\n""")

def test3():
print convert("""line 1\n\n![foo](asd)\n""")

def test4():
print convert("""line 1\n\n![foo](asd)\nNot blank\n""")


Note: I am using ^\s*\n to match a blank line - i.e. it can also contain spaces.

• That's great, but I find it highly unreadable. Would maybe a shorter regex just for is_image (something like !$.*$$$.*$$) be better, with the rest of the code intact? – Sam Estep Aug 31 '15 at 12:08
• Only you can answer that. I preserved the logic of your original program with the regex, i.e. checking for the blank lines before and after, etc. If the shorter one works for you, go ahead and use it! – ErikR Aug 31 '15 at 12:51
• OK, thanks. Just to make sure, is the regex !$.*$$$.*$$ equivalent to the Python expression line.startswith('![') and '](' in line and line.endswith(')')? – Sam Estep Aug 31 '15 at 12:54
• Use: ^!$.*$$$.*$$\n -- not the ^ at the beginning and the \n at the end. – ErikR Aug 31 '15 at 12:58
• I thought you were applying it to the entire file. If you are applying to an already stripped single line, the use \$ instead of \n. – ErikR Aug 31 '15 at 13:15

You could make your code more reusable (for example, from another Python program) by making it into a function. The descriptive comment would then naturally be the docstring of this function, which allows it to be accessed by other tools, like the help function in an interactive Python session, or the pydoc command-line tool.

To allow both using the function from another program (by importing it) and running it as a standalone script, use the __name__ == '__main__' idiom.

Concerning opening a file, 'r' is the default mode and does not need to be specified.

So, change this:

import sys

# Convert standalone images in standard Markdown
# to inline images in Pandoc's Markdown
with open(sys.argv[1], 'r') as markdown:
# ... process lines ...


To this:

def inline_images(markdown):
"""
Convert standalone images in standard Markdown contained in the
open file "markdown" to inline images in Pandoc's Markdown

• readlines does not work; it causes each line to still contain its trailing \n character. (I am using Python 3, if that matters.) – Sam Estep Aug 31 '15 at 14:51