1 
   2 """
   3 New latex formatter using dvipng and tempfile
   4 
   5 Author: JohannesBerg <johannes@sipsolutions.net>
   6 
   7 This parser (and the corresponding macro) was tested with Python 2.3.4 and
   8  * Debian Linux with out-of-the-box tetex-bin and dvipng packages installed
   9  * Windows XP (not by me)
  10  
  11 In the parser, you can add stuff to the prologue by writing 
  12 %%end-prologue%%
  13 somewhere in the document, before that write stuff like \\usepackage and after it put
  14 the actual latex display code.
  15 """
  16 
  17 Dependencies = []
  18 
  19 import sha, os, tempfile, shutil, re
  20 from MoinMoin.action import AttachFile
  21 from MoinMoin.Page import Page
  22 
  23 latex_template = r'''
  24 \documentclass[12pt]{article}
  25 \pagestyle{empty}
  26 \usepackage[utf8]{inputenc}
  27 %(prologue)s
  28 \begin{document}
  29 %(raw)s
  30 \end{document}
  31 '''
  32 
  33 max_pages = 10
  34 MAX_RUN_TIME = 5 # seconds
  35 
  36 latex = "latex"    # edit full path here, e.g. reslimit = "C:\\path\\to\\latex.exe"
  37 dvipng = "dvipng"  # edit full path here (or reslimit = r'C:\path\to\latex.exe')
  38 
  39 # last arg must have %s in it!
  40 latex_args = ("--interaction=nonstopmode", "%s.tex")
  41 
  42 # last arg must have %s in it!
  43 dvipng_args = ("-bgTransparent", "-Ttight", "--noghostscript", "-l%s" % max_pages, "%s.dvi")
  44 
  45 # this is formatted with hexdigest(texcode),
  46 # page number and extension are appended by
  47 # the tools
  48 latex_name_template = "latex_%s_p"
  49 
  50 # keep this up-to-date, also with max_pages!!
  51 latex_attachment = re.compile((latex_name_template+'%s%s') % (r'[0-9a-fA-F]{40}', r'[0-9]{1,2}', r'\.png'))
  52 
  53 anchor = re.compile(r'^%%anchor:[ ]*([a-zA-Z0-9_-]+)$', re.MULTILINE | re.IGNORECASE)
  54 # the anchor re must start with a % sign to be ignored by latex as a comment!
  55 end_prologue = '%%end-prologue%%'
  56 
  57 def call_command_in_dir_NT(app, args, targetdir):
  58     reslimit = "runlimit.exe" # edit full path here
  59     os.environ['openin_any'] = 'p'
  60     os.environ['openout_any'] = 'p'
  61     os.environ['shell_escape'] = 'f'
  62     stdouterr = os.popen('%s %d "%s" %s %s < NUL' % (reslimit, MAX_RUN_TIME, targetdir, app, ' '.join(args)), 'r')
  63     output = ''.join(stdouterr.readlines())
  64     err = stdouterr.close()
  65     if not err is None:
  66         return ' error! exitcode was %d, transscript follows:\n\n%s' % (err,output)
  67     return None
  68 
  69 def call_command_in_dir_unix(app, args, targetdir):
  70     # this is the unix implementation
  71     (r,w) = os.pipe()
  72     pid = os.fork()
  73     if pid == -1:
  74       return 'could not fork'
  75     if pid == 0:
  76       # child
  77       os.close(r)
  78       os.dup2(os.open("/dev/null", os.O_WRONLY), 0)
  79       os.dup2(w, 1)
  80       os.dup2(w, 2)
  81       os.chdir(targetdir)
  82       os.environ['openin_any'] = 'p'
  83       os.environ['openout_any'] = 'p'
  84       os.environ['shell_escape'] = 'f'
  85       import resource
  86       resource.setrlimit(resource.RLIMIT_CPU,
  87                          (MAX_RUN_TIME * 1000, MAX_RUN_TIME * 1000)) # docs say this is seconds, but it is msecs on my system.
  88       # os.execvp will raise an exception if the executable isn't
  89       # present. [[ try os.execvp("aoeu", ['aoeu'])  ]]
  90       # If we don't catch exceptions here, it will be caught at the
  91       # main body below, and then os.rmdir(tmpdir) will be called
  92       # twice, once for each fork.  The second one raises an exception
  93       # in the main code, which gets back to the user.  This is bad.
  94       try:
  95         os.execvp(app, [app] + list(args))
  96       finally:
  97         print "failed to exec()",app
  98         os._exit(2)
  99     else:
 100       # parent
 101       os.close(w)
 102       r = os.fdopen(r,"r")
 103       output = ''.join(r.readlines())
 104       (npid, exi) = os.waitpid(pid, 0)
 105       r.close()
 106       sig = exi & 0xFF
 107       stat = exi >> 8
 108       if stat != 0 or sig != 0:
 109         return ' error! exitcode was %d (signal %d), transscript follows:\n\n%s' % (stat,sig,output)
 110       return None
 111     # notreached      
 112 
 113 if os.name == 'nt':
 114     call_command_in_dir = call_command_in_dir_NT
 115 else:
 116     call_command_in_dir = call_command_in_dir_unix
 117 
 118 
 119 class Parser:
 120     extensions = ['.tex']
 121     def __init__ (self, raw, request, **kw):
 122         self.raw = raw
 123         if len(self.raw)>0 and self.raw[0] == '#':
 124             self.raw[0] = '%'
 125         self.request = request
 126         self.exclude = []
 127         if not hasattr(request, "latex_cleanup_done"):
 128             request.latex_cleanup_done = {}
 129 
 130     def cleanup(self, pagename):
 131         attachdir = AttachFile.getAttachDir(self.request, pagename, create=1)
 132         for f in os.listdir(attachdir):
 133             if not latex_attachment.match(f) is None:
 134                 os.remove("%s/%s" % (attachdir, f))
 135 
 136     def _internal_format(self, formatter, text):
 137         tmp = text.split(end_prologue, 1)
 138         if len(tmp) == 2:
 139             prologue,tex=tmp
 140         else:
 141             prologue = ''
 142             tex = tmp[0]
 143         if callable(getattr(formatter, 'johill_sidecall_emit_latex', None)):
 144           return formatter.johill_sidecall_emit_latex(tex)
 145         return self.get(formatter, tex, prologue, True)
 146 
 147     def format(self, formatter):
 148         self.request.write(self._internal_format(formatter, self.raw))
 149 
 150     def get(self, formatter, inputtex, prologue, para=False):
 151         if not self.request.latex_cleanup_done.has_key(self.request.page.page_name):
 152             self.request.latex_cleanup_done[self.request.page.page_name] = True
 153             self.cleanup(self.request.page.page_name)
 154 
 155         if len(inputtex) == 0: return ''
 156 
 157         if callable(getattr(formatter, 'johill_sidecall_emit_latex', None)):
 158           return formatter.johill_sidecall_emit_latex(inputtex)
 159 
 160         extra_preamble = ''
 161         preamble_page = self.request.pragma.get('latex_preamble', None)
 162         if preamble_page is not None:
 163           extra_preamble = Page(self.request, preamble_page).get_raw_body()
 164         extra_preamble = re.sub(re.compile('^#'), '%', extra_preamble)
 165 
 166         tex = latex_template % { 'raw': inputtex, 'prologue': extra_preamble + prologue }
 167         enctex = tex.encode('utf-8')
 168         fn = latex_name_template % sha.new(enctex).hexdigest()
 169 
 170         attachdir = AttachFile.getAttachDir(self.request, formatter.page.page_name, create=1)
 171         dst = "%s/%s%%d.png" % (attachdir, fn)
 172         if not os.access(dst % 1, os.R_OK):
 173           tmpdir = tempfile.mkdtemp()
 174           try:
 175               data = open("%s/%s.tex" % (tmpdir, fn), "w")
 176               data.write(enctex)
 177               data.close()
 178               args = list(latex_args)
 179               args[-1] = args[-1] % fn
 180               res = call_command_in_dir(latex, args, tmpdir)
 181               if not res is None:
 182                   return formatter.preformatted(1)+formatter.text('latex'+res)+formatter.preformatted(0)
 183               args = list(dvipng_args)
 184               args[-1] = args[-1] % fn
 185               res = call_command_in_dir(dvipng, args, tmpdir)
 186               if not res is None:
 187                   return formatter.preformatted(1)+formatter.text('dvipng'+res)+formatter.preformatted(0)
 188 
 189               page = 1
 190               while os.access("%s/%s%d.png" % (tmpdir, fn, page), os.R_OK):
 191                   shutil.copyfile ("%s/%s%d.png" % (tmpdir, fn, page), dst % page)
 192                   page += 1
 193 
 194           finally:
 195               for root,dirs,files in os.walk(tmpdir, topdown=False):
 196                   for name in files:
 197                       os.remove(os.path.join(root,name))
 198                   for name in dirs:
 199                       os.rmdir(os.path.join(root,name))
 200               os.rmdir(tmpdir)
 201 
 202         result = ""
 203         page = 1
 204         loop = False
 205         for match in anchor.finditer(inputtex):
 206             result += formatter.anchordef(match.group(1))
 207         for match in anchor.finditer(prologue):
 208             result += formatter.anchordef(match.group(1))
 209         while os.access(dst % page, os.R_OK):
 210             url = AttachFile.getAttachUrl(formatter.page.page_name, fn+"%d.png" % page, self.request)
 211             if loop:
 212                 result += formatter.linebreak(0)+formatter.linebreak(0)
 213             if para:
 214                 result += formatter.paragraph(1)
 215             result += formatter.image(src="%s" % url, alt=inputtex, title=inputtex, align="absmiddle")
 216             if para:
 217                 result += formatter.paragraph(0)
 218             page += 1
 219             loop = True
 220         return result