fix LaTeX breakage
[PyX/mjg.git] / pyx / text.py
blobcd88c227ad1d3904294dc9ede637b7fa7f48c8da
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2004 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
25 import glob, os, threading, Queue, traceback, re, tempfile, sys, atexit, time
26 import config, unit, box, canvas, trafo, version, attr, style, dvifile
28 ###############################################################################
29 # texmessages
30 # - please don't get confused:
31 # - there is a texmessage (and a texmessageparsed) attribute within the
32 # texrunner; it contains TeX/LaTeX response from the last command execution
33 # - instances of classes derived from the class texmessage are used to
34 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
35 # attribute of a texrunner instance
36 # - the multiple usage of the name texmessage might be removed in the future
37 # - texmessage instances should implement _Itexmessage
38 ###############################################################################
40 class TexResultError(RuntimeError):
41 """specialized texrunner exception class
42 - it is raised by texmessage instances, when a texmessage indicates an error
43 - it is raised by the texrunner itself, whenever there is a texmessage left
44 after all parsing of this message (by texmessage instances)"""
46 def __init__(self, description, texrunner):
47 self.description = description
48 self.texrunner = texrunner
50 def __str__(self):
51 """prints a detailed report about the problem
52 - the verbose level is controlled by texrunner.errordebug"""
53 if self.texrunner.errordebug >= 2:
54 return ("%s\n" % self.description +
55 "The expression passed to TeX was:\n"
56 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
57 "The return message from TeX was:\n"
58 " %s\n" % self.texrunner.texmessage.replace("\n", "\n ").rstrip() +
59 "After parsing this message, the following was left:\n"
60 " %s" % self.texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
61 elif self.texrunner.errordebug == 1:
62 firstlines = self.texrunner.texmessageparsed.split("\n")
63 if len(firstlines) > 5:
64 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
65 return ("%s\n" % self.description +
66 "The expression passed to TeX was:\n"
67 " %s\n" % self.texrunner.expr.replace("\n", "\n ").rstrip() +
68 "After parsing the return message from TeX, the following was left:\n" +
69 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
70 else:
71 return self.description
74 class TexResultWarning(TexResultError):
75 """as above, but with different handling of the exception
76 - when this exception is raised by a texmessage instance,
77 the information just get reported and the execution continues"""
78 pass
81 class _Itexmessage:
82 """validates/invalidates TeX/LaTeX response"""
84 def check(self, texrunner):
85 """check a Tex/LaTeX response and respond appropriate
86 - read the texrunners texmessageparsed attribute
87 - if there is an problem found, raise an appropriate
88 exception (TexResultError or TexResultWarning)
89 - remove any valid and identified TeX/LaTeX response
90 from the texrunners texmessageparsed attribute
91 -> finally, there should be nothing left in there,
92 otherwise it is interpreted as an error"""
95 class texmessage(attr.attr): pass
98 class _texmessagestart(texmessage):
99 """validates TeX/LaTeX startup"""
101 __implements__ = _Itexmessage
103 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
105 def check(self, texrunner):
106 m = self.startpattern.search(texrunner.texmessageparsed)
107 if not m:
108 raise TexResultError("TeX startup failed", texrunner)
109 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
110 try:
111 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
112 except (IndexError, ValueError):
113 raise TexResultError("TeX running startup file failed", texrunner)
114 try:
115 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
116 except (IndexError, ValueError):
117 raise TexResultError("TeX scrollmode check failed", texrunner)
120 class _texmessagenoaux(texmessage):
121 """allows for LaTeXs no-aux-file warning"""
123 __implements__ = _Itexmessage
125 def check(self, texrunner):
126 try:
127 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
128 texrunner.texmessageparsed = s1 + s2
129 except (IndexError, ValueError):
130 try:
131 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
132 os.sep,
133 texrunner.texfilename), 1)
134 texrunner.texmessageparsed = s1 + s2
135 except (IndexError, ValueError):
136 pass
139 class _texmessageinputmarker(texmessage):
140 """validates the PyXInputMarker"""
142 __implements__ = _Itexmessage
144 def check(self, texrunner):
145 try:
146 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
147 texrunner.texmessageparsed = s1 + s2
148 except (IndexError, ValueError):
149 raise TexResultError("PyXInputMarker expected", texrunner)
152 class _texmessagepyxbox(texmessage):
153 """validates the PyXBox output"""
155 __implements__ = _Itexmessage
157 pattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
159 def check(self, texrunner):
160 m = self.pattern.search(texrunner.texmessageparsed)
161 if m and m.group("page") == str(texrunner.page):
162 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
163 else:
164 raise TexResultError("PyXBox expected", texrunner)
167 class _texmessagepyxpageout(texmessage):
168 """validates the dvi shipout message (writing a page to the dvi file)"""
170 __implements__ = _Itexmessage
172 def check(self, texrunner):
173 try:
174 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
175 texrunner.texmessageparsed = s1 + s2
176 except (IndexError, ValueError):
177 raise TexResultError("PyXPageOutMarker expected", texrunner)
180 class _texmessagetexend(texmessage):
181 """validates TeX/LaTeX finish"""
183 __implements__ = _Itexmessage
185 def check(self, texrunner):
186 try:
187 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
188 texrunner.texmessageparsed = s1 + s2
189 except (IndexError, ValueError):
190 try:
191 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
192 os.sep,
193 texrunner.texfilename), 1)
194 texrunner.texmessageparsed = s1 + s2
195 except (IndexError, ValueError):
196 pass
197 try:
198 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
199 texrunner.texmessageparsed = s1 + s2
200 except (IndexError, ValueError):
201 pass
202 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
203 m = dvipattern.search(texrunner.texmessageparsed)
204 if texrunner.page:
205 if not m:
206 raise TexResultError("TeX dvifile messages expected", texrunner)
207 if m.group("page") != str(texrunner.page):
208 raise TexResultError("wrong number of pages reported", texrunner)
209 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
210 else:
211 try:
212 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 raise TexResultError("no dvifile expected", texrunner)
216 try:
217 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
218 texrunner.texmessageparsed = s1 + s2
219 except (IndexError, ValueError):
220 raise TexResultError("TeX logfile message expected", texrunner)
223 class _texmessageemptylines(texmessage):
224 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines"""
226 __implements__ = _Itexmessage
228 def check(self, texrunner):
229 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
230 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
233 class _texmessageload(texmessage):
234 """validates inclusion of arbitrary files
235 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
236 <fielname> is a readable file and other stuff can be anything
237 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
238 - this is not always wanted, but we just assume that file inclusion is fine"""
240 __implements__ = _Itexmessage
242 pattern = re.compile(r" *\((?P<filename>[^()\s\n]+)[^()]*\) *")
244 def baselevels(self, s, maxlevel=1, brackets="()"):
245 """strip parts of a string above a given bracket level
246 - return a modified (some parts might be removed) version of the string s
247 where all parts inside brackets with level higher than maxlevel are
248 removed
249 - if brackets do not match (number of left and right brackets is wrong
250 or at some points there were more right brackets than left brackets)
251 just return the unmodified string"""
252 level = 0
253 highestlevel = 0
254 res = ""
255 for c in s:
256 if c == brackets[0]:
257 level += 1
258 if level > highestlevel:
259 highestlevel = level
260 if level <= maxlevel:
261 res += c
262 if c == brackets[1]:
263 level -= 1
264 if level == 0 and highestlevel > 0:
265 return res
267 def check(self, texrunner):
268 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
269 if lowestbracketlevel is not None:
270 m = self.pattern.search(lowestbracketlevel)
271 while m:
272 if os.access(m.group("filename"), os.R_OK):
273 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
274 else:
275 break
276 m = self.pattern.search(lowestbracketlevel)
277 else:
278 texrunner.texmessageparsed = lowestbracketlevel
281 class _texmessageloadfd(_texmessageload):
282 """validates the inclusion of font description files (fd-files)
283 - works like _texmessageload
284 - filename must end with .fd and no further text is allowed"""
286 pattern = re.compile(r" *\((?P<filename>[^)]+.fd)\) *")
289 class _texmessagegraphicsload(_texmessageload):
290 """validates the inclusion of files as the graphics packages writes it
291 - works like _texmessageload, but using "<" and ">" as delimiters
292 - filename must end with .eps and no further text is allowed"""
294 pattern = re.compile(r" *<(?P<filename>[^>]+.eps)> *")
296 def baselevels(self, s, brackets="<>", **args):
297 return _texmessageload.baselevels(self, s, brackets=brackets, **args)
300 class _texmessageignore(_texmessageload):
301 """validates any TeX/LaTeX response
302 - this might be used, when the expression is ok, but no suitable texmessage
303 parser is available
304 - PLEASE: - consider writing suitable tex message parsers
305 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
307 __implements__ = _Itexmessage
309 def check(self, texrunner):
310 texrunner.texmessageparsed = ""
313 texmessage.start = _texmessagestart()
314 texmessage.noaux = _texmessagenoaux()
315 texmessage.inputmarker = _texmessageinputmarker()
316 texmessage.pyxbox = _texmessagepyxbox()
317 texmessage.pyxpageout = _texmessagepyxpageout()
318 texmessage.texend = _texmessagetexend()
319 texmessage.emptylines = _texmessageemptylines()
320 texmessage.load = _texmessageload()
321 texmessage.loadfd = _texmessageloadfd()
322 texmessage.graphicsload = _texmessagegraphicsload()
323 texmessage.ignore = _texmessageignore()
326 ###############################################################################
327 # textattrs
328 ###############################################################################
330 _textattrspreamble = ""
332 class textattr:
333 "a textattr defines a apply method, which modifies a (La)TeX expression"
335 class halign(attr.exclusiveattr, textattr):
337 def __init__(self, hratio):
338 self.hratio = hratio
339 attr.exclusiveattr.__init__(self, halign)
341 def apply(self, expr):
342 return r"\gdef\PyXHAlign{%.5f}%s" % (self.hratio, expr)
344 halign.center = halign(0.5)
345 halign.right = halign(1)
346 halign.clear = attr.clearclass(halign)
347 halign.left = halign.clear
350 class _localattr: pass
352 class _mathmode(attr.attr, textattr, _localattr):
353 "math mode"
355 def apply(self, expr):
356 return r"$\displaystyle{%s}$" % expr
358 mathmode = _mathmode()
359 nomathmode = attr.clearclass(_mathmode)
362 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge", None, "tiny", "scriptsize", "footnotesize", "small"]
364 class size(attr.sortbeforeattr, textattr, _localattr):
365 "font size"
367 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
368 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
369 raise RuntimeError("either specify sizeindex or sizename")
370 attr.sortbeforeattr.__init__(self, [_mathmode])
371 if sizeindex is not None:
372 if sizeindex >= 0 and sizeindex < sizelist.index(None):
373 self.size = sizelist[sizeindex]
374 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
375 self.size = sizelist[sizeindex]
376 else:
377 raise IndexError("index out of sizelist range")
378 else:
379 self.size = sizename
381 def apply(self, expr):
382 return r"\%s{%s}" % (self.size, expr)
384 size.tiny = size(-4)
385 size.scriptsize = size.script = size(-3)
386 size.footnotesize = size.footnote = size(-2)
387 size.small = size(-1)
388 size.normalsize = size.normal = size(0)
389 size.large = size(1)
390 size.Large = size(2)
391 size.LARGE = size(3)
392 size.huge = size(4)
393 size.Huge = size(5)
396 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
398 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
400 top = 1
401 middle = 2
402 bottom = 3
404 def __init__(self, width, baseline=top):
405 self.width = width
406 self.baseline = baseline
407 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
409 def apply(self, expr):
410 if self.baseline == self.top:
411 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
412 elif self.baseline == self.middle:
413 return r"\linewidth%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize\linewidth{%s}}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize\linewidth{%s}}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
414 elif self.baseline == self.bottom:
415 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
416 else:
417 RuntimeError("invalid baseline argument")
419 class parbox(parbox_pt):
421 def __init__(self, width, **kwargs):
422 parbox_pt.__init__(self, unit.topt(width), **kwargs)
425 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
427 class valign(attr.sortbeforeexclusiveattr, textattr):
429 def __init__(self):
430 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
432 class _valigntop(valign):
434 def apply(self, expr):
435 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
437 class _valignmiddle(valign):
439 def apply(self, expr):
440 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
442 class _valignbottom(valign):
444 def apply(self, expr):
445 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
447 valign.top = _valigntop()
448 valign.middle = _valignmiddle()
449 valign.bottom = _valignbottom()
450 valign.clear = attr.clearclass(valign)
451 valign.baseline = valign.clear
454 class _vshift(attr.sortbeforeattr, textattr):
456 def __init__(self):
457 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
459 class vshift(_vshift):
460 "vertical down shift by a fraction of a character height"
462 def __init__(self, lowerratio, heightstr="0"):
463 _vshift.__init__(self)
464 self.lowerratio = lowerratio
465 self.heightstr = heightstr
467 def apply(self, expr):
468 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
470 class _vshiftmathaxis(_vshift):
471 "vertical down shift by the height of the math axis"
473 def apply(self, expr):
474 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
477 vshift.bottomzero = vshift(0)
478 vshift.middlezero = vshift(0.5)
479 vshift.topzero = vshift(1)
480 vshift.mathaxis = _vshiftmathaxis()
483 ###############################################################################
484 # texrunner
485 ###############################################################################
488 class _readpipe(threading.Thread):
489 """threaded reader of TeX/LaTeX output
490 - sets an event, when a specific string in the programs output is found
491 - sets an event, when the terminal ends"""
493 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
494 """initialize the reader
495 - pipe: file to be read from
496 - expectqueue: keeps the next InputMarker to be wait for
497 - gotevent: the "got InputMarker" event
498 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
499 - quitevent: the "end of terminal" event"""
500 threading.Thread.__init__(self)
501 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
502 self.pipe = pipe
503 self.expectqueue = expectqueue
504 self.gotevent = gotevent
505 self.gotqueue = gotqueue
506 self.quitevent = quitevent
507 self.expect = None
508 self.start()
510 def run(self):
511 """thread routine"""
512 read = self.pipe.readline() # read, what comes in
513 try:
514 self.expect = self.expectqueue.get_nowait() # read, what should be expected
515 except Queue.Empty:
516 pass
517 while len(read):
518 # universal EOL handling (convert everything into unix like EOLs)
519 read.replace("\r", "")
520 if not len(read) or read[-1] != "\n":
521 read += "\n"
522 self.gotqueue.put(read) # report, whats readed
523 if self.expect is not None and read.find(self.expect) != -1:
524 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
525 read = self.pipe.readline() # read again
526 try:
527 self.expect = self.expectqueue.get_nowait()
528 except Queue.Empty:
529 pass
530 # EOF reached
531 self.pipe.close()
532 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
533 raise RuntimeError("TeX/LaTeX finished unexpectedly")
534 self.quitevent.set()
537 class textbox_pt(box.rect_pt, canvas._canvas):
538 """basically a box.rect, but it contains a text created by the texrunner
539 - texrunner._text and texrunner.text return such an object
540 - _textbox instances can be inserted into a canvas
541 - the output is contained in a page of the dvifile available thru the texrunner"""
542 # TODO: shouldn't all boxes become canvases? how about inserts then?
544 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
546 - finishdvi is a method to be called to get the dvicanvas
547 (e.g. the finishdvi calls the setdvicanvas method)
548 - attrs are fillstyles"""
549 self.texttrafo = trafo.translate_pt(x, y)
550 box.rect_pt.__init__(self, x - left, y - depth,
551 left + right, depth + height,
552 abscenter = (left, depth))
553 canvas._canvas.__init__(self)
554 self.finishdvi = finishdvi
555 self.dvicanvas = None
556 self.set(attrs)
557 self.insertdvicanvas = 0
559 def transform(self, *trafos):
560 if self.insertdvicanvas:
561 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
562 box.rect_pt.transform(self, *trafos)
563 for trafo in trafos:
564 self.texttrafo = trafo * self.texttrafo
566 def setdvicanvas(self, dvicanvas):
567 if self.dvicanvas is not None:
568 raise RuntimeError("multiple call to setdvicanvas")
569 self.dvicanvas = dvicanvas
571 def ensuredvicanvas(self):
572 if self.dvicanvas is None:
573 self.finishdvi()
574 assert self.dvicanvas is not None, "finishdvi is broken"
575 if not self.insertdvicanvas:
576 self.insert(self.dvicanvas, [self.texttrafo])
578 def marker(self, marker):
579 self.ensuredvicanvas()
580 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
582 def prolog(self):
583 self.ensuredvicanvas()
584 return canvas._canvas.prolog(self)
586 def write(self, file):
587 self.ensuredvicanvas()
588 canvas._canvas.write(self, file)
591 class textbox(textbox_pt):
593 def __init__(self, x, y, left, right, height, depth, texrunner, attrs):
594 textbox_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
595 unit.topt(height), unit.topt(depth), texrunner, attrs)
598 def _cleantmp(texrunner):
599 """get rid of temporary files
600 - function to be registered by atexit
601 - files contained in usefiles are kept"""
602 if texrunner.texruns: # cleanup while TeX is still running?
603 texrunner.texruns = 0
604 texrunner.texdone = 1
605 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
606 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
607 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
608 else:
609 texrunner.texinput.write("\n\\end\n")
610 texrunner.texinput.close() # close the input queue and
611 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
612 return # didn't got a quit from TeX -> we can't do much more
613 for usefile in texrunner.usefiles:
614 extpos = usefile.rfind(".")
615 try:
616 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
617 except OSError:
618 pass
619 for file in glob.glob("%s.*" % texrunner.texfilename):
620 try:
621 os.unlink(file)
622 except OSError:
623 pass
624 if texrunner.texdebug is not None:
625 try:
626 texrunner.texdebug.close()
627 texrunner.texdebug = None
628 except IOError:
629 pass
632 class texrunner:
633 """TeX/LaTeX interface
634 - runs TeX/LaTeX expressions instantly
635 - checks TeX/LaTeX response
636 - the instance variable texmessage stores the last TeX
637 response as a string
638 - the instance variable texmessageparsed stores a parsed
639 version of texmessage; it should be empty after
640 texmessage.check was called, otherwise a TexResultError
641 is raised
642 - the instance variable errordebug controls the verbose
643 level of TexResultError"""
645 def __init__(self, mode="tex",
646 lfs="10pt",
647 docclass="article",
648 docopt=None,
649 usefiles=[],
650 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
651 waitfortex=config.getint("text", "waitfortex", 60),
652 showwaitfortex=config.getint("text", "showwaitfortex", 5),
653 texipc=config.getboolean("text", "texipc", 0),
654 texdebug=None,
655 dvidebug=0,
656 errordebug=1,
657 dvicopy=0,
658 pyxgraphics=1,
659 texmessagesstart=[texmessage.start],
660 texmessagesdocclass=[texmessage.load],
661 texmessagesbegindoc=[texmessage.load, texmessage.noaux],
662 texmessagesend=[texmessage.texend],
663 texmessagesdefaultpreamble=[texmessage.load],
664 texmessagesdefaultrun=[texmessage.loadfd, texmessage.graphicsload]):
665 mode = mode.lower()
666 if mode != "tex" and mode != "latex":
667 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
668 self.mode = mode
669 self.lfs = lfs
670 self.docclass = docclass
671 self.docopt = docopt
672 self.usefiles = usefiles
673 self.fontmap = dvifile.readfontmap(fontmaps.split())
674 self.waitfortex = waitfortex
675 self.showwaitfortex = showwaitfortex
676 self.texipc = texipc
677 if texdebug is not None:
678 if texdebug[-4:] == ".tex":
679 self.texdebug = open(texdebug, "w")
680 else:
681 self.texdebug = open("%s.tex" % texdebug, "w")
682 else:
683 self.texdebug = None
684 self.dvidebug = dvidebug
685 self.errordebug = errordebug
686 self.dvicopy = dvicopy
687 self.pyxgraphics = pyxgraphics
688 self.texmessagesstart = texmessagesstart
689 self.texmessagesdocclass = texmessagesdocclass
690 self.texmessagesbegindoc = texmessagesbegindoc
691 self.texmessagesend = texmessagesend
692 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
693 self.texmessagesdefaultrun = texmessagesdefaultrun
695 self.texruns = 0
696 self.texdone = 0
697 self.preamblemode = 1
698 self.executeid = 0
699 self.page = 0
700 self.preambles = []
701 self.acttextboxes = [] # when texipc-mode off
702 self.actdvifile = None # when texipc-mode on
703 savetempdir = tempfile.tempdir
704 tempfile.tempdir = os.curdir
705 self.texfilename = os.path.basename(tempfile.mktemp())
706 tempfile.tempdir = savetempdir
708 def waitforevent(self, event):
709 """waits verbosely with an timeout for an event
710 - observes an event while periodly while printing messages
711 - returns the status of the event (isSet)
712 - does not clear the event"""
713 if self.showwaitfortex:
714 waited = 0
715 hasevent = 0
716 while waited < self.waitfortex and not hasevent:
717 if self.waitfortex - waited > self.showwaitfortex:
718 event.wait(self.showwaitfortex)
719 waited += self.showwaitfortex
720 else:
721 event.wait(self.waitfortex - waited)
722 waited += self.waitfortex - waited
723 hasevent = event.isSet()
724 if not hasevent:
725 if waited < self.waitfortex:
726 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
727 else:
728 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
729 return hasevent
730 else:
731 event.wait(self.waitfortex)
732 return event.isSet()
734 def execute(self, expr, texmessages):
735 """executes expr within TeX/LaTeX
736 - if self.texruns is not yet set, TeX/LaTeX is initialized,
737 self.texruns is set and self.preamblemode is set
738 - the method must not be called, when self.texdone is already set
739 - expr should be a string or None
740 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
741 while self.texdone becomes set
742 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
743 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
744 - texmessages is a list of texmessage instances"""
745 if not self.texruns:
746 if self.texdebug is not None:
747 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
748 self.texdebug.write("%% mode: %s\n" % self.mode)
749 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
750 for usefile in self.usefiles:
751 extpos = usefile.rfind(".")
752 try:
753 os.rename(usefile, self.texfilename + usefile[extpos:])
754 except OSError:
755 pass
756 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
757 texfile.write("\\relax%\n")
758 texfile.close()
759 if self.texipc:
760 ipcflag = " --ipc"
761 else:
762 ipcflag = ""
763 try:
764 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
765 except ValueError:
766 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
767 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
768 atexit.register(_cleantmp, self)
769 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
770 self.gotevent = threading.Event() # keeps the got inputmarker event
771 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
772 self.quitevent = threading.Event() # keeps for end of terminal event
773 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
774 self.texruns = 1
775 oldpreamblemode = self.preamblemode
776 self.preamblemode = 1
777 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
778 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
779 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
780 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
781 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
782 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
783 "\\newdimen\\PyXDimenHAlignRT%\n" +
784 _textattrspreamble + # insert preambles for textattrs macros
785 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
786 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
787 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
788 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
789 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
790 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
791 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
792 "lt=\\the\\PyXDimenHAlignLT,"
793 "rt=\\the\\PyXDimenHAlignRT,"
794 "ht=\\the\\ht\\PyXBox,"
795 "dp=\\the\\dp\\PyXBox:}%\n"
796 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
797 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
798 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
799 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
800 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
801 attr.mergeattrs(self.texmessagesstart))
802 os.remove("%s.tex" % self.texfilename)
803 if self.mode == "tex":
804 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
805 lfsname = self.lfs
806 else:
807 lfsname = "%s.lfs" % self.lfs
808 for fulllfsname in [lfsname,
809 os.path.join(sys.prefix, "share", "pyx", lfsname),
810 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
811 try:
812 lfsfile = open(fulllfsname, "r")
813 lfsdef = lfsfile.read()
814 lfsfile.close()
815 break
816 except IOError:
817 pass
818 else:
819 allfiles = (glob.glob("*.lfs") +
820 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
821 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
822 lfsnames = []
823 for f in allfiles:
824 try:
825 open(f, "r").close()
826 lfsnames.append(os.path.basename(f)[:-4])
827 except IOError:
828 pass
829 lfsnames.sort()
830 if len(lfsnames):
831 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
832 else:
833 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
834 self.execute(lfsdef, [])
835 self.execute("\\normalsize%\n", [])
836 self.execute("\\newdimen\\linewidth%\n", [])
837 elif self.mode == "latex":
838 if self.pyxgraphics:
839 for pyxdef in ["pyx.def",
840 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
841 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
842 try:
843 open(pyxdef, "r").close()
844 break
845 except IOError:
846 pass
847 else:
848 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
849 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
850 self.execute("\\makeatletter%\n"
851 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
852 "\\def\\ProcessOptions{%\n"
853 "\\def\\Gin@driver{" + pyxdef + "}%\n"
854 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
855 "\\saveProcessOptions}%\n"
856 "\\makeatother",
858 if self.docopt is not None:
859 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
860 attr.mergeattrs(self.texmessagesdocclass))
861 else:
862 self.execute("\\documentclass{%s}" % self.docclass,
863 attr.mergeattrs(self.texmessagesdocclass))
864 self.preamblemode = oldpreamblemode
865 self.executeid += 1
866 if expr is not None: # TeX/LaTeX should process expr
867 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
868 if self.preamblemode:
869 self.expr = ("%s%%\n" % expr +
870 "\\PyXInput{%i}%%\n" % self.executeid)
871 else:
872 self.page += 1
873 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
874 "\\PyXInput{%i}%%\n" % self.executeid)
875 else: # TeX/LaTeX should be finished
876 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
877 if self.mode == "latex":
878 self.expr = "\\end{document}%\n"
879 else:
880 self.expr = "\\end%\n"
881 if self.texdebug is not None:
882 self.texdebug.write(self.expr)
883 self.texinput.write(self.expr)
884 gotevent = self.waitforevent(self.gotevent)
885 self.gotevent.clear()
886 if expr is None and gotevent: # TeX/LaTeX should have finished
887 self.texruns = 0
888 self.texdone = 1
889 self.texinput.close() # close the input queue and
890 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
891 try:
892 self.texmessage = ""
893 while 1:
894 self.texmessage += self.gotqueue.get_nowait()
895 except Queue.Empty:
896 pass
897 self.texmessageparsed = self.texmessage
898 if gotevent:
899 if expr is not None:
900 texmessage.inputmarker.check(self)
901 if not self.preamblemode:
902 texmessage.pyxbox.check(self)
903 texmessage.pyxpageout.check(self)
904 for checktexmessage in texmessages:
905 try:
906 checktexmessage.check(self)
907 except TexResultWarning:
908 traceback.print_exc()
909 texmessage.emptylines.check(self)
910 if len(self.texmessageparsed):
911 raise TexResultError("unhandled TeX response (might be an error)", self)
912 else:
913 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
915 def finishdvi(self):
916 """finish TeX/LaTeX and read the dvifile
917 - this method ensures that all textboxes can access their
918 dvicanvas"""
919 self.execute(None, self.texmessagesend)
920 if self.dvicopy:
921 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
922 dvifilename = "%s.dvicopy" % self.texfilename
923 else:
924 dvifilename = "%s.dvi" % self.texfilename
925 if not self.texipc:
926 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
927 for box in self.acttextboxes:
928 box.setdvicanvas(self.dvifile.readpage())
929 if self.dvifile.readpage() is not None:
930 raise RuntimeError("end of dvifile expected")
931 self.dvifile = None
932 self.acttextboxes = []
934 def reset(self, reinit=0):
935 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
936 if self.texruns:
937 self.finishdvi()
938 if self.texdebug is not None:
939 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
940 self.executeid = 0
941 self.page = 0
942 self.texdone = 0
943 if reinit:
944 self.preamblemode = 1
945 for expr, texmessages in self.preambles:
946 self.execute(expr, texmessages)
947 if self.mode == "latex":
948 self.execute("\\begin{document}", self.texmessagesbegindoc)
949 self.preamblemode = 0
950 else:
951 self.preambles = []
952 self.preamblemode = 1
954 def set(self, mode=None,
955 lfs=None,
956 docclass=None,
957 docopt=None,
958 usefiles=None,
959 fontmaps=None,
960 waitfortex=None,
961 showwaitfortex=None,
962 texipc=None,
963 texdebug=None,
964 dvidebug=None,
965 errordebug=None,
966 dvicopy=None,
967 pyxgraphics=None,
968 texmessagesstart=None,
969 texmessagesdocclass=None,
970 texmessagesbegindoc=None,
971 texmessagesend=None,
972 texmessagesdefaultpreamble=None,
973 texmessagesdefaultrun=None):
974 """provide a set command for TeX/LaTeX settings
975 - TeX/LaTeX must not yet been started
976 - especially needed for the defaultrunner, where no access to
977 the constructor is available"""
978 if self.texruns:
979 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
980 if mode is not None:
981 mode = mode.lower()
982 if mode != "tex" and mode != "latex":
983 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
984 self.mode = mode
985 if lfs is not None:
986 self.lfs = lfs
987 if docclass is not None:
988 self.docclass = docclass
989 if docopt is not None:
990 self.docopt = docopt
991 if usefiles is not None:
992 self.usefiles = usefiles
993 if fontmaps is not None:
994 self.fontmap = dvifile.readfontmap(fontmaps.split())
995 if waitfortex is not None:
996 self.waitfortex = waitfortex
997 if showwaitfortex is not None:
998 self.showwaitfortex = showwaitfortex
999 if texipc is not None:
1000 self.texipc = texipc
1001 if texdebug is not None:
1002 if self.texdebug is not None:
1003 self.texdebug.close()
1004 if texdebug[-4:] == ".tex":
1005 self.texdebug = open(texdebug, "w")
1006 else:
1007 self.texdebug = open("%s.tex" % texdebug, "w")
1008 if dvidebug is not None:
1009 self.dvidebug = dvidebug
1010 if errordebug is not None:
1011 self.errordebug = errordebug
1012 if dvicopy is not None:
1013 self.dvicopy = dvicopy
1014 if pyxgraphics is not None:
1015 self.pyxgraphics = pyxgraphics
1016 if errordebug is not None:
1017 self.errordebug = errordebug
1018 if texmessagesstart is not None:
1019 self.texmessagesstart = texmessagesstart
1020 if texmessagesdocclass is not None:
1021 self.texmessagesdocclass = texmessagesdocclass
1022 if texmessagesbegindoc is not None:
1023 self.texmessagesbegindoc = texmessagesbegindoc
1024 if texmessagesend is not None:
1025 self.texmessagesend = texmessagesend
1026 if texmessagesdefaultpreamble is not None:
1027 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1028 if texmessagesdefaultrun is not None:
1029 self.texmessagesdefaultrun = texmessagesdefaultrun
1031 def preamble(self, expr, texmessages=[]):
1032 r"""put something into the TeX/LaTeX preamble
1033 - in LaTeX, this is done before the \begin{document}
1034 (you might use \AtBeginDocument, when you're in need for)
1035 - it is not allowed to call preamble after calling the
1036 text method for the first time (for LaTeX this is needed
1037 due to \begin{document}; in TeX it is forced for compatibility
1038 (you should be able to switch from TeX to LaTeX, if you want,
1039 without breaking something)
1040 - preamble expressions must not create any dvi output
1041 - args might contain texmessage instances"""
1042 if self.texdone or not self.preamblemode:
1043 raise RuntimeError("preamble calls disabled due to previous text calls")
1044 texmessages = attr.mergeattrs(texmessages, self.texmessagesdefaultpreamble)
1045 self.execute(expr, texmessages)
1046 self.preambles.append((expr, texmessages))
1048 PyXBoxPattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
1050 def text_pt(self, x, y, expr, textattrs=[], texmessages=[]):
1051 """create text by passing expr to TeX/LaTeX
1052 - returns a textbox containing the result from running expr thru TeX/LaTeX
1053 - the box center is set to x, y
1054 - *args may contain attr parameters, namely:
1055 - textattr instances
1056 - texmessage instances
1057 - trafo._trafo instances
1058 - style.fillstyle instances"""
1059 if expr is None:
1060 raise ValueError("None expression is invalid")
1061 if self.texdone:
1062 self.reset(reinit=1)
1063 first = 0
1064 if self.preamblemode:
1065 if self.mode == "latex":
1066 self.execute("\\begin{document}", self.texmessagesbegindoc)
1067 self.preamblemode = 0
1068 first = 1
1069 if self.texipc and self.dvicopy:
1070 raise RuntimeError("texipc and dvicopy can't be mixed up")
1071 textattrs = attr.mergeattrs(textattrs)
1072 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1073 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1074 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1075 textattrs = attr.getattrs(textattrs, [textattr])
1076 lentextattrs = len(textattrs)
1077 for i in range(lentextattrs):
1078 expr = textattrs[lentextattrs-1-i].apply(expr)
1079 self.execute(expr, attr.mergeattrs(texmessages, self.texmessagesdefaultrun))
1080 if self.texipc:
1081 if first:
1082 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1083 match = self.PyXBoxPattern.search(self.texmessage)
1084 if not match or int(match.group("page")) != self.page:
1085 raise TexResultError("box extents not found", self)
1086 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
1087 box = textbox_pt(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1088 for t in trafos:
1089 box.reltransform(t)
1090 if self.texipc:
1091 box.setdvicanvas(self.dvifile.readpage())
1092 self.acttextboxes.append(box)
1093 return box
1095 def text(self, x, y, expr, *args, **kwargs):
1096 return self.text_pt(unit.topt(x), unit.topt(y), expr, *args, **kwargs)
1099 # the module provides an default texrunner and methods for direct access
1100 defaulttexrunner = texrunner()
1101 reset = defaulttexrunner.reset
1102 set = defaulttexrunner.set
1103 preamble = defaulttexrunner.preamble
1104 text = defaulttexrunner.text
1105 text_pt = defaulttexrunner.text_pt