text: #!/usr/bin/env python3
  
  from bottle import route
  import bottle
  import tempfile
  import os
  import shutil
  import subprocess
  import json
  import hashlib
  
  def is_known_format(fmt):
      return fmt in ['std', 'export', 'simple', 'inline']
  
  def get_out_filename(name, fmt = 'std'):
      if not is_known_format(fmt): raise Exception('Unknown format')
      f =  os.path.join(outdir, get_out_filename_rel(name, fmt))
      if not os.path.abspath(f).startswith(os.path.abspath(outdir)):
          raise Exception('Path out problem', os.path.abspath(f), os.path.abspath(outdir))
      return f
  
  def get_out_filename_rel(name, fmt = 'std'):
      if not is_known_format(fmt): raise Exception('Unknown format')
      return "{}.{}.html".format(name, fmt)
  
  def get_in_filename(name):
      f = os.path.join(indir, get_in_filename_rel(name))
      if not os.path.abspath(f).startswith(os.path.abspath(indir)):
          raise Exception('Path in problem', os.path.abspath(f), os.path.abspath(indir))
      for file_ending in [""] + file_endings:
          if os.path.exists(f + file_ending):
              return f + file_ending
      return f
  
  def get_in_filename_rel(name):
      return name
  
  def has_compile_file_ending(f):
      for file_ending in file_endings:
          if f.endswith(file_ending):
              return True
      return False
  
  @route('/view/<name:path>')
  def route_view(name):
      infile = get_in_filename(name)
  
      if os.path.exists(infile) and has_compile_file_ending(infile):
          fmt = bottle.request.query.fmt or "std"
          if not is_known_format(fmt): return 'Unknown format'
  
          compile_document(name, fmt)
          return bottle.static_file(get_out_filename_rel(name, fmt), outdir)
  
      else:
          return bottle.static_file(get_in_filename_rel(name), indir)
  
  @route('/refresh/<name:path>')
  def route_refresh(name):
      if needs_update(name):
          return "True"
      return "False"
  
  @route('/generated/<name:path>')
  def route_generated(name):
      return bottle.static_file(name, os.path.join(tempdir, "generated"))
  
  @route('/')
  def route_index():
      text = ""
  
      text += "<html><head><title>Panserver Index</title>"
      text += '<style type="text/css">{}</style>'.format(style_basic + style_index)
      text += "</head><body>"
      text += '<h1>Panserver</h1>'
      text += 'Serving Markdown documents rendered using <a href="http://pandoc.org/">pandoc</a>. By <a href="http://marcelfischer.eu/">Marcel Fischer</a>'
  
      def dir_entry(dirname, toplevel = False):
          #collect markdown files recursively into a list
          #return '' if no markdown file is in the directory
          dirtext = ''
          print("dirname ", dirname)
          for name in sorted(os.listdir(os.path.join('.', dirname))):
              path = os.path.join(dirname, name)
              if os.path.isdir(path): continue
              if not has_compile_file_ending(path): continue
              d = {}
              d['name'] = os.path.basename(path)
              d['path'] = path
              dirtext += '<li class="file-entry"><a href="/view/{path}">{name}</a></li>'.format(**d)
  
          for name in sorted(os.listdir(os.path.join('.', dirname))):
              path = os.path.join(dirname, name)
              if not os.path.isdir(path): continue
              subdirtext = dir_entry(path)
              #ignore directories with empty listings
              if subdirtext != '':
                  dirtext += '<li class="dir-entry">' + os.path.basename(path) + subdirtext + '</li>'
  
          if dirtext != '':
              dirtext = '<ul class="file-listing">' + dirtext + '</ul>'
          elif toplevel:
              dirtext = '<i> (no documents)</i>'
          return dirtext
  
      text += "<h3>Directory contents</h3>"
      text += dir_entry('', toplevel=True)
  
      text += '</body></html>'
  
      return text
  
  def needs_update(name, fmt = 'std'):
      out_filename = get_out_filename(name, fmt)
      in_filename = get_in_filename(name)
      if not os.path.exists(in_filename): return False
      if not os.path.exists(out_filename): return True
      return os.path.getmtime(out_filename) < os.path.getmtime(in_filename)
  
  def compile_document(name, fmt):
      out_filename = get_out_filename(name, fmt)
      in_filename = get_in_filename(name)
      if needs_update(name, fmt):
          #file is not cached, recompile
          #create directory if needed
          out_filename_dir = os.path.dirname(out_filename)
          if not os.path.exists(out_filename_dir):
              os.makedirs(out_filename_dir)
  
          #combine action based on format
          action = ['pandoc']
          # Format to use GFM"
          action.append("--from=gfm")
  
          if fmt == 'std':
              action += ['-H', headerfile, '-B', topmenufile,]
  
          if fmt == 'export':
              action += ['-H', headerfile_export,]
  
          if fmt == 'std' or fmt == 'export':
              action += ['-B', beforefile, '-A', afterfile,]
              action += ['--toc']
  
          if fmt != 'inline':
              action += ['-s']
  
          # compile
          #print(action)
          # create json in intermediate step
          json_encoding_process = subprocess.Popen(action + ['-t', 'json', in_filename], stdout=subprocess.PIPE)
          json_text, _ = json_encoding_process.communicate()
          json_text = json_text.decode('utf-8')
  
          # process json to extract embedded dot diagrams and others
          json_text, math_option = process_document_json(json_text, alternative_title=os.path.basename(name))
  
          # render to HTML
          json_decoding_process = subprocess.Popen(action + math_option + ['-f', 'json', '-o', out_filename], stdin=subprocess.PIPE)
          json_decoding_process.communicate(json_text.encode('utf-8'))
  
  def process_document_json(json_text, alternative_title):
      math_option = ['--mathjax']
      changed = False
  
      doc = json.loads(json_text)
  
      # TODO descent recursively into json doc
  
      for block in doc["blocks"]:
          if block["t"] == "CodeBlock":
              if "dot" in block["c"][0][1]:
                  extract_embedding(block, "dot")
                  changed = True
              elif "plantuml" in block["c"][0][1]:
                  extract_embedding(block, "plantuml")
                  changed = True
  
      # Add title if missing
      if "title" not in doc["meta"] and "pagetitle" not in doc["meta"]:
          doc["meta"]["pagetitle"] = {'t': 'MetaInlines', 'c': [{'t': 'Str', 'c': alternative_title}]}
          changed = True
  
      # Allow user to select math option using metadata
      if "panserver_math" in doc["meta"]:
          meta_math = doc["meta"]["panserver_math"]["c"][0]["c"]
          print("math option ", meta_math)
          if meta_math == "mathml":
              math_option = ['--mathml']
          elif meta_math == "mathjax":
              math_option = ['--mathjax']
          elif meta_math == "none":
              math_option = []
  
      if changed:
          json_text = json.dumps(doc)
      return json_text, math_option
  
  def extract_embedding(block, embedding_type):
      code = block["c"][1]
      md5 = hashlib.md5(code.encode('utf-8')).hexdigest()
      generation_dir = os.path.join(tempdir, "generated")
      error = None
      if not os.path.exists(generation_dir):
          os.makedirs(generation_dir)
      target = os.path.join(generation_dir, "{}.{}.png".format(md5, embedding_type))
      if not os.path.exists(target):
          if embedding_type == "dot":
              process = subprocess.Popen(["dot", "-Tpng"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          elif embedding_type == "plantuml":
              process = subprocess.Popen(["plantuml", "-pipe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          process_out, process_err = process.communicate(code.encode('utf-8'))
          error_code = process.wait()
          if error_code != 0:
              error = process_err.decode('utf-8')
          else:
              with open(target, "wb") as f:
                  f.write(process_out)
  
      if error is None:
          # Replace embedding in block with image
          block["t"] = "Para"
          block["c"] = [ { "t" : "Image", "c" : [
              ["", [], []],
              [],
              [ "/generated/{}.{}.png".format(md5, embedding_type), "fig:"]
              ]}]
      else:
          # Insert error
          block["c"][0][1] = []
          block["c"][1] = error
  
  def create_header(autorefresh):
      headertext = ""
      headertext += """{}<style type="text/css">{}</style>""".format(meta_no_mobilescale, style_basic + style_document_add)
      headertext += markdown_css_link
  
      if autorefresh:
          headertext += """
      <script>
      window.setInterval(function() {
          var xhr = new XMLHttpRequest();
          var href = window.location.href;
          var re = /view\/(.+)$/;
          var name = re.exec(href)[1];
          xhr.open('GET', '/refresh/' + name);
          xhr.onload = function() {
              if (xhr.status === 200) {
                  if(xhr.responseText == 'True') {
                      window.location.reload();
                  }
              }
              else {
                  //alert('Request failed! ' + xhr.status);
              }
          };
          xhr.send();
      }, 750);
      </script>
  """
  
      with open(headerfile, 'w') as f:
          f.write(headertext)
  
  def create_header_export():
      text = """{}<style type="text/css">{}</style>""".format(meta_no_mobilescale, style_basic)
      text += markdown_css_link
  
      with open(headerfile_export, 'w') as f:
          f.write(text)
  
  def create_topmenufile():
      text = """
      <span class="topmenu">Panserver: <a href="/">Index</a>
      <span style="text-decoration: underline; cursor: pointer" onclick="(tocElement = document.getElementById('TOC')).style.display = (tocElement.style.display != 'block') ? 'block' : 'none';">TOC</span>
      Format:
      <a href="?fmt=export">Export</a>
      <a href="?fmt=simple">Simple</a>
      <a href="?fmt=inline">Inline</a>
      </span>
      """
      with open(topmenufile, 'w') as f:
          f.write(text)
  
  def create_beforefile():
      text = """
      <span class="markdown-body">
      """
      with open(beforefile, 'w') as f:
          f.write(text)
  
  def create_afterfile():
      with open(afterfile, 'w') as f:
          f.write("</span>")
  
  #global vars
  file_endings = [".md", ".markdown", ".rst"]
  tempdir = tempfile.mkdtemp()
  outdir = os.path.join(tempdir, "out")
  indir = os.path.abspath('.')
  
  headerfile = os.path.join(tempdir, "header.html")
  headerfile_export = os.path.join(tempdir, "header.export.html")
  beforefile = os.path.join(tempdir, "before.html")
  topmenufile = os.path.join(tempdir, "topmenu.html")
  afterfile = os.path.join(tempdir, "after.html")
  
  if not os.path.exists(outdir):
      os.makedirs(outdir)
  
  meta_no_mobilescale = """<meta name="viewport" content="width=device-width, initial-scale=1.0">"""
  
  
  style_basic = """
          body {
              width: 92%; max-width: 40em;  margin: auto;
              font-size: 1.2rem; line-height: 1.5; padding-bottom: 3em;}
          code { font-family: "DejaVu Sans Mono", Consolas, monospace; }
          pre { overflow: auto; }
          math, .LaTeX { font-size: 1.1em; }
          div.figure .caption {  padding: 0em 1em 1em; font-size: 0.8em; }
          ul li { list-style-type: disc; }
          li p { margin: 0.5em 0; }
          #TOC { border: 1px solid lightgray; padding: 0.5em; margin-bottom: 1em;}
          #TOC li { list-style: circle; }
          @media (min-width: 102em) {
              body { position: relative; }
              #TOC { display: block; position: absolute; left: -50%; top: 5em; max-width: 46%; width: 45%; }
          }
  """
  
  style_document_add = """
          @media (max-width: 101em) {
              #TOC { display: none; }
          }
          .topmenu { font-size: 1em; color: lightgrey; }
          .topmenu a { color: lightgrey; }
  """
  style_index = """
          ul.file-listing a { text-decoration: none; }
          ul.file-listing ul.file-listing { padding-left: 1.5em; }
          ul li.dir-entry {
              list-style-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAApElEQVQ4je3UsQqBYRTG8Z9SMpjsrsAdYMI1WNyDhdnKriiD1U3YXML3SWE3uAgDX+lF8iqlPPX01qnn/55OncMvqYMUSeA1KjHAFKUH9S7GMcDkSb2Jk/vOQ2/QhzKq2F/fd52/ftzCBKZYYRnhA2ohcIb6m+PJdJv9A//AT4GND4BZtp0BBxgiFwG8zY7QgyIWLsv96gCE3uKIHeYoRDT1ZZ0BsR1JIXdX8A8AAAAASUVORK5CYII=');
          }
          ul li.file-entry{
              list-style-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAA00lEQVQ4jcXUQUqCURSG4Qc0bKaUiKsIhEbazMBhOHILES3EUYIGtpBwlDODhhFCTqQdtIYGf5H+eOVeFP4P3ul7L4fzHbZziQme9jDEqYhU8IkervbwjecYaRPziIeXuIuRNvESKRQjjRV+4f6XN8xQPkQ4wO0Ga9QPEeazKER4gi6uA3T9zy1KeCZb8McAY1RThCkpRtjAh2yJd/EuG0txPzy68BxT4RM2RS1FWEZH+IS1UUoRpuQ1JPw7sDfCDcnTx0rWrJ1pYSTckDwPuNgU/ADJ7ku1B60bXwAAAABJRU5ErkJggg==');
          }
  """
  
  markdown_css_link= """<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/3.0.1/github-markdown.css">"""
  
  if __name__ == '__main__':
      import webbrowser
      import argparse
  
      parser = argparse.ArgumentParser(description='Run a local markdown compiling server')
  
      parser.add_argument('-a', '--no-refresh', action='store_false',
                          help="Don't refresh the page",
                          dest='autorefresh')
      parser.add_argument('-p', '--port', type=int, default=10543,
                          help="Port to be opened, default is 10543",
                         )
      parser.add_argument('-b', '--no-open-browser', action='store_false',
                          help="Don't open browser",
                          dest='open_browser')
      parser.add_argument('-r', action='store_true',
                          help="""Allow remote access to this server.
                          Warning! can be extremely dangerous!"""
                         )
      parser.add_argument('path', nargs='?', help="Path to directory")
      config = parser.parse_args()
  
      create_header(config.autorefresh)
      create_header_export()
      create_topmenufile()
      create_beforefile()
      create_afterfile()
  
      if config.path != None:
          if os.path.isdir(config.path):
              os.chdir(config.path)
              indir = os.path.abspath('.')
          else:
              raise Exception('Unknown path argument')
  
      if config.open_browser:
          webbrowser.get().open('http://localhost:{}/'.format(config.port))
  
      host = 'localhost'
      if config.r:
          host = ''
  
      bottle.run(host=host, port=config.port)
      shutil.rmtree(outdir)
  
  