""" TracMath - A trac plugin that renders latex formulas within a wiki page. This has currently been tested only on trac 0.10.4 and 0.11. """ import codecs import re import sha from cStringIO import StringIO import os import sys from genshi.builder import tag from trac.core import * from trac.wiki.api import IWikiMacroProvider from trac.wiki.api import IWikiSyntaxProvider from trac.mimeview.api import IHTMLPreviewRenderer, MIME_MAP from trac.web import IRequestHandler from trac.util import escape from trac.wiki.formatter import wiki_to_oneliner from trac import mimeview __author__ = 'Reza Lotun' __author_email__ = 'rlotun@gmail.com' tex_preamble = r""" \documentclass{article} \usepackage{amsmath} \usepackage{amsthm} \usepackage{amssymb} \usepackage{bm} \pagestyle{empty} \begin{document} """ rePNG = re.compile(r'.+png$') reGARBAGE = [ re.compile(r'.+aux$'), re.compile(r'.+log$'), re.compile(r'.+tex$'), re.compile(r'.+dvi$'), ] reLABEL = re.compile(r'\\label\{(.*?)\}') class TracMathPlugin(Component): implements(IWikiMacroProvider, IHTMLPreviewRenderer, IRequestHandler, IWikiSyntaxProvider) def __init__(self): self.load_config() def load_config(self): """Load the tracmath trac.ini configuration.""" # defaults tmp = '/tmp/tracmath' latex = '/usr/bin/latex' dvipng = '/usr/bin/dvipng' max_png = 500 if 'tracmath' not in self.config.sections(): pass # TODO: do something self.cacheDirectory = self.config.get('tracmath', 'cache_dir') or tmp self.latex_cmd = self.config.get('tracmath', 'latex_cmd') or latex self.dvipng_cmd = self.config.get('tracmath', 'dvipng_cmd') or dvipng self.max_png = self.config.get('tracmath', 'max_png') or max_png self.max_png = int(self.max_png) self.use_dollars = self.config.get('tracmath', 'use_dollars') or "False" self.use_dollars = self.use_dollars.lower() in ("true", "on", "enabled") if not os.path.exists(self.cacheDirectory): os.mkdir(self.cacheDirectory, 0777) #TODO: check correct values. return '' def show_err(self, msg): """Display msg in an error box, using Trac style.""" buf = StringIO() buf.write('
\n\ TracMath macro processor has detected an \n\ error. Please fix the problem before continuing. \n\
%s
\n\
' % escape(msg)) self.log.error(msg) return buf # IWikiSyntaxProvider methods def get_wiki_syntax(self): def format(formatter, ns, match): return self.internal_render(formatter.req,'latex',match.group(0)) yield (r"\$[^$]+\$", format) def get_link_resolvers(self): return [] # IWikiSyntaxProvider methods # stolen from http://trac-hacks.org/ticket/248 def get_wiki_syntax(self): if self.use_dollars: yield (r"\$\$(?P.*?)\$\$", lambda formatter, ns, match: "
" + self.expand_macro(formatter, 'latex', ns) + "
") yield (r"\$(?P.*?)\$", lambda formatter, ns, match: self.expand_macro(formatter, 'latex', ns)) def get_link_resolvers(self): return [] # IWikiMacroProvider methods def get_macros(self): yield 'latex' def get_macro_description(self, name): if name == 'latex': return """ This plugin allows embedded equations in Trac markup. Basically a port of http://www.amk.ca/python/code/mt-math to Trac. Simply use {{{ {{{ #!latex [latex code] }}} }}} for a block of LaTeX code. If `use_dollars` is enabled in `trac.ini`, then you can also use `$[latex formula]$` for inline math or `$$[latex formula]$$` for display math. """ def internal_render(self, req, name, content): if not name == 'latex': return 'Unknown macro %s' % (name) label = None for line in content.split("\n"): m = reLABEL.match(content) if m: label = m.group(1) key = sha.new(content.encode('utf-8')).hexdigest() imgname = key + '.png' imgpath = os.path.join(self.cacheDirectory, imgname) if not os.path.exists(imgpath): texname = key + '.tex' texpath = os.path.join(self.cacheDirectory, texname) try: f = codecs.open(texpath, encoding='utf-8', mode='w') f.write(tex_preamble) f.write(content) f.write('\end{document}') f.close() except Exception, e: return self.show_err("Problem creating tex file: %s" % (e)) os.chdir(self.cacheDirectory) cmd = self.latex_cmd + ' %s' % texname pin, pout, perr = os.popen3(cmd) pin.close() out = pout.read() err = perr.read() if len(err) and len(out): return 'Unable to call: %s %s %s' % (cmd, out, err) cmd = "".join([self.dvipng_cmd, " -T tight -x 1200 -z 9 -bg Transparent ", "-o %s %s" % (imgname, key + '.dvi')]) pin, pout, perr = os.popen3(cmd) pin.close() out = pout.read() err = perr.read() if len(err) and len(out): pass # TODO: check for real errors self.manage_cache() result = '%s' % (req.base_url, imgname, content) if label: result = '(%s) %s' % (label, label, result) return result def manage_cache(self): ftime = [] for name in os.listdir(self.cacheDirectory): for ext in reGARBAGE: if ext.match(name): os.unlink(os.path.join(self.cacheDirectory, name)) if rePNG.match(name): info = os.stat(os.path.join(self.cacheDirectory, name)) ftime.append((info[7], name)) ftime.sort(reverse=True) numfiles = len(ftime) files = (name for _, name in ftime) while numfiles > self.max_png: name = files.next() os.unlink(os.path.join(self.cacheDirectory, name)) numfiles -= 1 def expand_macro(self, formatter, name, content): return self.internal_render(formatter.req, name, content) # needed for Trac 0.10.4 def render_macro(self, req, name, content): return self.internal_render(req, name, content) # IHTMLPreviewRenderer methods def get_quality_ratio(self, mimetype): if mimetype in ('application/tracmath'): return 2 return 0 def render(self, req, mimetype, content, filename=None, url=None): text = hasattr(content, 'read') and content.read() or content return self.internal_render(req, name, text) # IRequestHandler methods def match_request(self, req): return req.path_info.startswith('/tracmath') def process_request(self, req): pieces = [item for item in req.path_info.split('/tracmath') if item] if pieces: pieces = [item for item in pieces[0].split('/') if item] if pieces: name = pieces[0] img_path = os.path.join(self.cacheDirectory, name) return req.send_file(img_path, mimeview.get_mimetype(img_path)) return