Skip to content
Open
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ desired command line to invoke `ditaa`, for example:
where `{infile}` and `{outfile}` are placeholders for input and output file
names.

### Testing
When running tests, be sure that your `PATH` contains the `ditaa` executable, or set `DTIAA_CMD` appropriately as described above.

Compatiblity: ditaa and fenced_code
-----------------------------------

Expand Down
132 changes: 93 additions & 39 deletions mdx_ditaa.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

# The MIT License (MIT)
#
#
# Copyright (c) 2014 Sergey Astanin
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -15,7 +15,7 @@
#
# 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
Expand All @@ -28,11 +28,11 @@
import ctypes
import os
import platform
import shutil
import subprocess
import tempfile
import zlib


from markdown.preprocessors import Preprocessor
from markdown.extensions import Extension

Expand All @@ -45,56 +45,76 @@ def b(string):
return string


DITAA_CMD = os.environ.get("DITAA_CMD", "ditaa {infile} {outfile} --overwrite")


def generate_image_path(plaintext):
adler32 = ctypes.c_uint32(zlib.adler32(b(plaintext))).value
imgbasename = "diagram-%x.png" % adler32
ditaa_image_dir = os.environ.get("DITAA_IMAGE_DIR", ".")
imgpath = os.path.join(ditaa_image_dir, imgbasename)
return imgpath


def generate_diagram(plaintext):
"""Run ditaa with plaintext input.
Return relative path to the generated image.
"""
imgpath = generate_image_path(plaintext)
srcfd, srcfname = tempfile.mkstemp(prefix="ditaasrc", text=True)
outfd, outfname = tempfile.mkstemp(prefix="ditaaout", text=True)
with os.fdopen(srcfd, "w") as src:
src.write(plaintext)
try:
cmd = DITAA_CMD.format(infile=srcfname, outfile=imgpath).split()
with os.fdopen(outfd, "w") as out:
retval = subprocess.check_call(cmd, stdout=out)
return os.path.relpath(imgpath, os.getcwd())
except:
return None
finally:
os.unlink(srcfname)
os.unlink(outfname)
class DitaaPreprocessor(Preprocessor):

def __init__(self, *args, **config):
self.config = config

def generate_image_path(self, plaintext):
"""
Return an image path based on a hash of the plaintext input.
"""
adler32 = ctypes.c_uint32(zlib.adler32(b(plaintext))).value
img_basename = "diagram-%x.png" % adler32
image_path = os.path.join(self.config['ditaa_image_dir'], img_basename)
return image_path

def generate_diagram(self, plaintext):
"""
Run ditaa with plaintext input.
Return relative path to the generated image.
"""
img_dest = self.generate_image_path(plaintext)

if os.path.exists(img_dest):
return os.path.relpath(img_dest, os.getcwd())

src_fd, src_fname = tempfile.mkstemp(prefix="ditaasrc", text=True)
out_fd, out_fname = tempfile.mkstemp(prefix="ditaaout", text=True)
with os.fdopen(src_fd, "w") as src:
src.write(plaintext)
try:
ditaa_cmd = self.config['ditaa_cmd']
cmd = ditaa_cmd.format(infile=src_fname, outfile=img_dest)
with os.fdopen(out_fd, "w") as out:
retval = subprocess.check_call(cmd.split(), stdout=out)

if self.config.get('extra_copy_path', None):
try:
shutil.copy(img_dest, self.config['extra_copy_path'])
except:
pass
return os.path.relpath(img_dest, os.getcwd())

except Exception as e:
return None
finally:
os.unlink(src_fname)
os.unlink(out_fname)

class DitaaPreprocessor(Preprocessor):
def run(self, lines):
START_TAG = "```ditaa"
END_TAG = "```"
new_lines = []
ditaa_prefix = ""
ditaa_lines = []
in_diagram = False
path_override = None
for ln in lines:
if in_diagram: # lines of a diagram
if ln == ditaa_prefix + END_TAG:
# strip line prefix if any (whitespace, bird marks)
plen = len(ditaa_prefix)
ditaa_lines = [dln[plen:] for dln in ditaa_lines]
ditaa_code = "\n".join(ditaa_lines)
filename = generate_diagram(ditaa_code)
filename = self.generate_diagram(ditaa_code)
if filename:
new_lines.append(ditaa_prefix + "![%s](%s)" % (filename, filename))
if path_override:
mkdocs_path = os.path.join(path_override, os.path.basename(filename))
else:
mkdocs_path = os.path.join(self.config['extra_copy_path'], os.path.basename(filename))
new_lines.append(ditaa_prefix + "![%s](%s)" % (mkdocs_path, mkdocs_path))
path_override = None
else:
md_code = [ditaa_prefix + " " + dln for dln in ditaa_lines]
new_lines.extend([""] + md_code + [""])
Expand All @@ -105,6 +125,11 @@ def run(self, lines):
else: # normal lines
start = ln.find(START_TAG)
prefix = ln[:start] if start >= 0 else ""
postfix = ln[start + len(START_TAG) + 1:] if start >= 0 else ""
if postfix and postfix.startswith('path='):
path_override = postfix[5:]
ln = ln[:-len(postfix) - 1]

# code block may be nested within a list item or a blockquote
if start >= 0 and ln.endswith(START_TAG) and not prefix.strip(" \t>"):
in_diagram = True
Expand All @@ -115,11 +140,40 @@ def run(self, lines):


class DitaaExtension(Extension):

PreprocessorClass = DitaaPreprocessor

def __init__(self, **kwargs):
ditaa_cmd = kwargs.get('ditaa_cmd', 'ditaa {infile} {outfile} --overwrite')
ditaa_image_dir = kwargs.get('ditaa_image_dir', '.')
extra_copy_path = kwargs.get('extra_copy_path', None)

if 'DITAA_CMD' in os.environ:
ditaa_cmd = os.environ.get("DITAA_CMD")
if 'DITAA_IMAGE_DIR' in os.environ:
ditaa_image_dir = os.environ.get("DITAA_IMAGE_DIR")

self.config = {
'ditaa_cmd': [ditaa_cmd,
"Full command line to launch ditaa. Defaults to:"
"{}".format(ditaa_cmd)],
'ditaa_image_dir': [ditaa_image_dir,
"Full path to directory where images will be saved."],
'extra_copy_path': [extra_copy_path,
"Set this path to save an extra copy into "
"the specified directory."]
}

super(DitaaExtension, self).__init__(**kwargs)

def extendMarkdown(self, md, md_globals):
ditaa_ext = self.PreprocessorClass(md, self.config)
ditaa_ext.config = self.getConfigs()

md.registerExtension(self)
location = "<fenced_code" if ("fenced_code" in md.preprocessors) else "_begin"
md.preprocessors.add("ditaa", DitaaPreprocessor(md), location)
md.preprocessors.add("ditaa", ditaa_ext, location)


def makeExtension(configs=None):
return DitaaExtension(configs=configs)
def makeExtension(**kwargs):
return DitaaExtension(**kwargs)
7 changes: 7 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from setuptools import setup
setup(
name='mdx_ditaa',
version='0.3',
py_modules=['mdx_ditaa'],
install_requires = ['markdown>=2.5'],
)