dvifile separation
[PyX/mjg.git] / pyx / text.py
blob85a7324a5547758191aa7f7d4cbfec8a8f6e1558
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2003 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2002-2003 André Wobst <wobsta@users.sourceforge.net>
7 # Copyright (C) 2003 Michael Schindler <m-schindler@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 copy, cStringIO, exceptions, glob, os, threading, Queue, traceback, re, struct, string, tempfile, sys, atexit, time
26 import config, helper, unit, box, canvas, trafo, pykpathsea, 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(Exception):
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: 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")
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")
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, expr, sizelist=defaultsizelist):
368 attr.sortbeforeattr.__init__(self, [_mathmode])
369 if helper.isinteger(expr):
370 if expr >= 0 and expr < sizelist.index(None):
371 self.size = sizelist[expr]
372 elif expr < 0 and expr + len(sizelist) > sizelist.index(None):
373 self.size = sizelist[expr]
374 else:
375 raise IndexError("index out of sizelist range")
376 else:
377 self.size = expr
379 def apply(self, expr):
380 return r"\%s{%s}" % (self.size, expr)
382 for s in defaultsizelist:
383 if s is not None:
384 setattr(size, s, size(s))
387 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
389 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
391 top = 1
392 middle = 2
393 bottom = 3
395 def __init__(self, width, baseline=top):
396 self.width = width
397 self.baseline = baseline
398 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
400 def apply(self, expr):
401 if self.baseline == self.top:
402 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
403 elif self.baseline == self.middle:
404 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)
405 elif self.baseline == self.bottom:
406 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{%s}}" % (self.width * 72.27 / 72, expr)
407 else:
408 RuntimeError("invalid baseline argument")
410 class parbox(parbox_pt):
412 def __init__(self, width, **kwargs):
413 parbox_pt.__init__(self, unit.topt(width), **kwargs)
416 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
418 class valign(attr.sortbeforeexclusiveattr, textattr):
420 def __init__(self):
421 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
423 class _valigntop(valign):
425 def apply(self, expr):
426 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\lower\ht\PyXBoxVAlign\box\PyXBoxVAlign" % expr
428 class _valignmiddle(valign):
430 def apply(self, expr):
431 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=0.5\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -0.5\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % expr
433 class _valignbottom(valign):
435 def apply(self, expr):
436 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\raise\dp\PyXBoxVAlign\box\PyXBoxVAlign" % expr
438 valign.top = _valigntop()
439 valign.middle = _valignmiddle()
440 valign.bottom = _valignbottom()
441 valign.clear = attr.clearclass(valign)
442 valign.baseline = valign.clear
445 class _vshift(attr.sortbeforeattr, textattr):
447 def __init__(self):
448 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
450 class vshift(_vshift):
451 "vertical down shift by a fraction of a character height"
453 def __init__(self, lowerratio, heightstr="0"):
454 _vshift.__init__(self)
455 self.lowerratio = lowerratio
456 self.heightstr = heightstr
458 def apply(self, expr):
459 return r"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self.heightstr, self.lowerratio, expr)
461 class _vshiftmathaxis(_vshift):
462 "vertical down shift by the height of the math axis"
464 def apply(self, expr):
465 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
468 vshift.bottomzero = vshift(0)
469 vshift.middlezero = vshift(0.5)
470 vshift.topzero = vshift(1)
471 vshift.mathaxis = _vshiftmathaxis()
474 ###############################################################################
475 # texrunner
476 ###############################################################################
479 class _readpipe(threading.Thread):
480 """threaded reader of TeX/LaTeX output
481 - sets an event, when a specific string in the programs output is found
482 - sets an event, when the terminal ends"""
484 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
485 """initialize the reader
486 - pipe: file to be read from
487 - expectqueue: keeps the next InputMarker to be wait for
488 - gotevent: the "got InputMarker" event
489 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
490 - quitevent: the "end of terminal" event"""
491 threading.Thread.__init__(self)
492 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
493 self.pipe = pipe
494 self.expectqueue = expectqueue
495 self.gotevent = gotevent
496 self.gotqueue = gotqueue
497 self.quitevent = quitevent
498 self.expect = None
499 self.start()
501 def run(self):
502 """thread routine"""
503 read = self.pipe.readline() # read, what comes in
504 try:
505 self.expect = self.expectqueue.get_nowait() # read, what should be expected
506 except Queue.Empty:
507 pass
508 while len(read):
509 # universal EOL handling (convert everything into unix like EOLs)
510 read.replace("\r", "")
511 if not len(read) or read[-1] != "\n":
512 read += "\n"
513 self.gotqueue.put(read) # report, whats readed
514 if self.expect is not None and read.find(self.expect) != -1:
515 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
516 read = self.pipe.readline() # read again
517 try:
518 self.expect = self.expectqueue.get_nowait()
519 except Queue.Empty:
520 pass
521 # EOF reached
522 self.pipe.close()
523 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
524 raise RuntimeError("TeX/LaTeX finished unexpectedly")
525 self.quitevent.set()
528 class textbox_pt(box._rect, canvas._canvas):
529 """basically a box.rect, but it contains a text created by the texrunner
530 - texrunner._text and texrunner.text return such an object
531 - _textbox instances can be inserted into a canvas
532 - the output is contained in a page of the dvifile available thru the texrunner"""
533 # TODO: shouldn't all boxes become canvases? how about inserts then?
535 def __init__(self, x, y, left, right, height, depth, finishdvi, *attrs):
537 - finishdvi is a method to be called to get the dvicanvas
538 (e.g. the finishdvi calls the setdvicanvas method)
539 - attrs are fillstyles"""
540 self.texttrafo = trafo._translate(x, y)
541 box._rect.__init__(self, x - left, y - depth,
542 left + right, depth + height,
543 abscenter = (left, depth))
544 canvas._canvas.__init__(self)
545 self.finishdvi = finishdvi
546 self.dvicanvas = None
547 for attr in attrs:
548 self.set(attr)
550 def transform(self, *trafos):
551 box._rect.transform(self, *trafos)
552 for trafo in trafos:
553 self.texttrafo = trafo * self.texttrafo
555 def setdvicanvas(self, dvicanvas):
556 self.insert(dvicanvas, self.texttrafo)
557 self.dvicanvas = dvicanvas
559 def ensuredvicanvas(self):
560 if self.dvicanvas is None:
561 self.finishdvi()
562 assert self.dvicanvas is not None, "finishdvi is broken"
564 def marker(self, marker):
565 self.ensuredvicanvas()
566 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
568 def prolog(self):
569 self.ensuredvicanvas()
570 return canvas._canvas.prolog(self)
572 def write(self, file):
573 self.ensuredvicanvas()
574 canvas._canvas.write(self, file)
577 class textbox(textbox_pt):
579 def __init__(self, x, y, left, right, height, depth, texrunner, *attrs):
580 textbox_pt.__init__(self, unit.topt(x), unit.topt(y), unit.topt(left), unit.topt(right),
581 unit.topt(height), unit.topt(depth), texrunner, *attrs)
584 def _cleantmp(texrunner):
585 """get rid of temporary files
586 - function to be registered by atexit
587 - files contained in usefiles are kept"""
588 if texrunner.texruns: # cleanup while TeX is still running?
589 texrunner.texruns = 0
590 texrunner.texdone = 1
591 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
592 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
593 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
594 else:
595 texrunner.texinput.write("\n\\end\n")
596 texrunner.texinput.close() # close the input queue and
597 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
598 return # didn't got a quit from TeX -> we can't do much more
599 for usefile in texrunner.usefiles:
600 extpos = usefile.rfind(".")
601 try:
602 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
603 except OSError:
604 pass
605 for file in glob.glob("%s.*" % texrunner.texfilename):
606 try:
607 os.unlink(file)
608 except OSError:
609 pass
610 if texrunner.texdebug is not None:
611 try:
612 texrunner.texdebug.close()
613 texrunner.texdebug = None
614 except IOError:
615 pass
618 # texrunner state exceptions
619 class TexRunsError(Exception): pass
620 class TexDoneError(Exception): pass
621 class TexNotInPreambleModeError(Exception): pass
624 class texrunner:
625 """TeX/LaTeX interface
626 - runs TeX/LaTeX expressions instantly
627 - checks TeX/LaTeX response
628 - the instance variable texmessage stores the last TeX
629 response as a string
630 - the instance variable texmessageparsed stores a parsed
631 version of texmessage; it should be empty after
632 texmessage.check was called, otherwise a TexResultError
633 is raised
634 - the instance variable errordebug controls the verbose
635 level of TexResultError"""
637 def __init__(self, mode="tex",
638 lfs="10pt",
639 docclass="article",
640 docopt=None,
641 usefiles=None,
642 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
643 waitfortex=config.getint("text", "waitfortex", 60),
644 showwaitfortex=config.getint("text", "showwaitfortex", 5),
645 texipc=config.getboolean("text", "texipc", 0),
646 texdebug=None,
647 dvidebug=0,
648 errordebug=1,
649 dvicopy=0,
650 pyxgraphics=1,
651 texmessagestart=texmessage.start,
652 texmessagedocclass=texmessage.load,
653 texmessagebegindoc=(texmessage.load, texmessage.noaux),
654 texmessageend=texmessage.texend,
655 texmessagedefaultpreamble=texmessage.load,
656 texmessagedefaultrun=texmessage.loadfd):
657 mode = mode.lower()
658 if mode != "tex" and mode != "latex":
659 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
660 self.mode = mode
661 self.lfs = lfs
662 self.docclass = docclass
663 self.docopt = docopt
664 self.usefiles = helper.ensurelist(usefiles)
665 self.fontmap = dvifile.readfontmap(fontmaps.split())
666 self.waitfortex = waitfortex
667 self.showwaitfortex = showwaitfortex
668 self.texipc = texipc
669 if texdebug is not None:
670 if texdebug[-4:] == ".tex":
671 self.texdebug = open(texdebug, "w")
672 else:
673 self.texdebug = open("%s.tex" % texdebug, "w")
674 else:
675 self.texdebug = None
676 self.dvidebug = dvidebug
677 self.errordebug = errordebug
678 self.dvicopy = dvicopy
679 self.pyxgraphics = pyxgraphics
680 texmessagestart = helper.ensuresequence(texmessagestart)
681 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
682 self.texmessagestart = texmessagestart
683 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
684 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
685 self.texmessagedocclass = texmessagedocclass
686 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
687 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
688 self.texmessagebegindoc = texmessagebegindoc
689 texmessageend = helper.ensuresequence(texmessageend)
690 helper.checkattr(texmessageend, allowmulti=(texmessage,))
691 self.texmessageend = texmessageend
692 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
693 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
694 self.texmessagedefaultpreamble = texmessagedefaultpreamble
695 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
696 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
697 self.texmessagedefaultrun = texmessagedefaultrun
699 self.texruns = 0
700 self.texdone = 0
701 self.preamblemode = 1
702 self.executeid = 0
703 self.page = 0
704 self.preambles = []
705 self.acttextboxes = [] # when texipc-mode off
706 self.actdvifile = None # when texipc-mode on
707 savetempdir = tempfile.tempdir
708 tempfile.tempdir = os.curdir
709 self.texfilename = os.path.basename(tempfile.mktemp())
710 tempfile.tempdir = savetempdir
712 def waitforevent(self, event):
713 """waits verbosely with an timeout for an event
714 - observes an event while periodly while printing messages
715 - returns the status of the event (isSet)
716 - does not clear the event"""
717 if self.showwaitfortex:
718 waited = 0
719 hasevent = 0
720 while waited < self.waitfortex and not hasevent:
721 if self.waitfortex - waited > self.showwaitfortex:
722 event.wait(self.showwaitfortex)
723 waited += self.showwaitfortex
724 else:
725 event.wait(self.waitfortex - waited)
726 waited += self.waitfortex - waited
727 hasevent = event.isSet()
728 if not hasevent:
729 if waited < self.waitfortex:
730 sys.stderr.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self.mode, waited))
731 else:
732 sys.stderr.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited, self.mode))
733 return hasevent
734 else:
735 event.wait(self.waitfortex)
736 return event.isSet()
738 def execute(self, expr, *checks):
739 """executes expr within TeX/LaTeX
740 - if self.texruns is not yet set, TeX/LaTeX is initialized,
741 self.texruns is set and self.preamblemode is set
742 - the method must not be called, when self.texdone is already set
743 - expr should be a string or None
744 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
745 while self.texdone becomes set
746 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
747 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
749 if not self.texruns:
750 if self.texdebug is not None:
751 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
752 self.texdebug.write("%% mode: %s\n" % self.mode)
753 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
754 for usefile in self.usefiles:
755 extpos = usefile.rfind(".")
756 try:
757 os.rename(usefile, self.texfilename + usefile[extpos:])
758 except OSError:
759 pass
760 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
761 texfile.write("\\relax%\n")
762 texfile.close()
763 if self.texipc:
764 ipcflag = " --ipc"
765 else:
766 ipcflag = ""
767 try:
768 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
769 except ValueError:
770 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
771 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
772 atexit.register(_cleantmp, self)
773 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
774 self.gotevent = threading.Event() # keeps the got inputmarker event
775 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
776 self.quitevent = threading.Event() # keeps for end of terminal event
777 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
778 self.texruns = 1
779 oldpreamblemode = self.preamblemode
780 self.preamblemode = 1
781 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
782 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
783 "\\gdef\\PyXHAlign{0}%\n" # global PyXHAlign (0.0-1.0) for the horizontal alignment, default to 0
784 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
785 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
786 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
787 "\\newdimen\\PyXDimenHAlignRT%\n" +
788 _textattrspreamble + # insert preambles for textattrs macros
789 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
790 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
791 "\\PyXDimenHAlignLT=\\PyXHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
792 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
793 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
794 "\\gdef\\PyXHAlign{0}%\n" # reset the PyXHAlign to the default 0
795 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
796 "lt=\\the\\PyXDimenHAlignLT,"
797 "rt=\\the\\PyXDimenHAlignRT,"
798 "ht=\\the\\ht\\PyXBox,"
799 "dp=\\the\\dp\\PyXBox:}%\n"
800 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
801 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
802 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
803 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
804 "\\def\\PyXMarker#1{\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
805 *self.texmessagestart)
806 os.remove("%s.tex" % self.texfilename)
807 if self.mode == "tex":
808 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
809 lfsname = self.lfs
810 else:
811 lfsname = "%s.lfs" % self.lfs
812 for fulllfsname in [lfsname,
813 os.path.join(sys.prefix, "share", "pyx", lfsname),
814 os.path.join(os.path.dirname(__file__), "lfs", lfsname)]:
815 try:
816 lfsfile = open(fulllfsname, "r")
817 lfsdef = lfsfile.read()
818 lfsfile.close()
819 break
820 except IOError:
821 pass
822 else:
823 allfiles = (glob.glob("*.lfs") +
824 glob.glob(os.path.join(sys.prefix, "share", "pyx", "*.lfs")) +
825 glob.glob(os.path.join(os.path.dirname(__file__), "lfs", "*.lfs")))
826 lfsnames = []
827 for f in allfiles:
828 try:
829 open(f, "r").close()
830 lfsnames.append(os.path.basename(f)[:-4])
831 except IOError:
832 pass
833 lfsnames.sort()
834 if len(lfsnames):
835 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname, lfsnames))
836 else:
837 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname)
838 self.execute(lfsdef)
839 self.execute("\\normalsize%\n")
840 self.execute("\\newdimen\\linewidth%\n")
841 elif self.mode == "latex":
842 if self.pyxgraphics:
843 for pyxdef in ["pyx.def",
844 os.path.join(sys.prefix, "share", "pyx", "pyx.def"),
845 os.path.join(os.path.dirname(__file__), "..", "contrib", "pyx.def")]:
846 try:
847 open(pyxdef, "r").close()
848 break
849 except IOError:
850 pass
851 else:
852 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
853 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
854 self.execute("\\makeatletter%\n"
855 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
856 "\\def\\ProcessOptions{%\n"
857 "\\def\\Gin@driver{" + pyxdef + "}%\n"
858 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
859 "\\saveProcessOptions}%\n"
860 "\\makeatother")
861 if self.docopt is not None:
862 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass), *self.texmessagedocclass)
863 else:
864 self.execute("\\documentclass{%s}" % self.docclass, *self.texmessagedocclass)
865 self.preamblemode = oldpreamblemode
866 self.executeid += 1
867 if expr is not None: # TeX/LaTeX should process expr
868 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
869 if self.preamblemode:
870 self.expr = ("%s%%\n" % expr +
871 "\\PyXInput{%i}%%\n" % self.executeid)
872 else:
873 self.page += 1
874 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
875 "\\PyXInput{%i}%%\n" % self.executeid)
876 else: # TeX/LaTeX should be finished
877 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
878 if self.mode == "latex":
879 self.expr = "\\end{document}%\n"
880 else:
881 self.expr = "\\end%\n"
882 if self.texdebug is not None:
883 self.texdebug.write(self.expr)
884 self.texinput.write(self.expr)
885 gotevent = self.waitforevent(self.gotevent)
886 self.gotevent.clear()
887 if expr is None and gotevent: # TeX/LaTeX should have finished
888 self.texruns = 0
889 self.texdone = 1
890 self.texinput.close() # close the input queue and
891 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
892 try:
893 self.texmessage = ""
894 while 1:
895 self.texmessage += self.gotqueue.get_nowait()
896 except Queue.Empty:
897 pass
898 self.texmessageparsed = self.texmessage
899 if gotevent:
900 if expr is not None:
901 texmessage.inputmarker.check(self)
902 if not self.preamblemode:
903 texmessage.pyxbox.check(self)
904 texmessage.pyxpageout.check(self)
905 for check in checks:
906 try:
907 check.check(self)
908 except TexResultWarning:
909 traceback.print_exc()
910 texmessage.emptylines.check(self)
911 if len(self.texmessageparsed):
912 raise TexResultError("unhandled TeX response (might be an error)", self)
913 else:
914 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
916 def finishdvi(self):
917 """finish TeX/LaTeX and read the dvifile
918 - this method ensures that all textboxes can access their
919 dvicanvas"""
920 self.execute(None, *self.texmessageend)
921 if self.dvicopy:
922 os.system("dvicopy %(t)s.dvi %(t)s.dvicopy > %(t)s.dvicopyout 2> %(t)s.dvicopyerr" % {"t": self.texfilename})
923 dvifilename = "%s.dvicopy" % self.texfilename
924 else:
925 dvifilename = "%s.dvi" % self.texfilename
926 if not self.texipc:
927 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
928 for box in self.acttextboxes:
929 box.setdvicanvas(self.dvifile.readpage())
930 if self.dvifile.readpage() is not None:
931 raise RuntimeError("end of dvifile expected")
932 self.dvifile = None
933 self.acttextboxes = []
935 def reset(self, reinit=0):
936 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
937 if self.texruns:
938 self.finishdvi()
939 if self.texdebug is not None:
940 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
941 self.executeid = 0
942 self.page = 0
943 self.texdone = 0
944 if reinit:
945 self.preamblemode = 1
946 for expr, args in self.preambles:
947 self.execute(expr, *args)
948 if self.mode == "latex":
949 self.execute("\\begin{document}", *self.texmessagebegindoc)
950 self.preamblemode = 0
951 else:
952 self.preambles = []
953 self.preamblemode = 1
955 def set(self, mode=None,
956 lfs=None,
957 docclass=None,
958 docopt=None,
959 usefiles=None,
960 fontmaps=None,
961 waitfortex=None,
962 showwaitfortex=None,
963 texipc=None,
964 texdebug=None,
965 dvidebug=None,
966 errordebug=None,
967 dvicopy=None,
968 pyxgraphics=None,
969 texmessagestart=None,
970 texmessagedocclass=None,
971 texmessagebegindoc=None,
972 texmessageend=None,
973 texmessagedefaultpreamble=None,
974 texmessagedefaultrun=None):
975 """provide a set command for TeX/LaTeX settings
976 - TeX/LaTeX must not yet been started
977 - especially needed for the defaultrunner, where no access to
978 the constructor is available"""
979 if self.texruns:
980 raise TexRunsError
981 if mode is not None:
982 mode = mode.lower()
983 if mode != "tex" and mode != "latex":
984 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
985 self.mode = mode
986 if lfs is not None:
987 self.lfs = lfs
988 if docclass is not None:
989 self.docclass = docclass
990 if docopt is not None:
991 self.docopt = docopt
992 if usefiles is not None:
993 self.usefiles = helper.ensurelist(usefiles)
994 if fontmaps is not None:
995 self.fontmap = readfontmap(fontmaps.split())
996 if waitfortex is not None:
997 self.waitfortex = waitfortex
998 if showwaitfortex is not None:
999 self.showwaitfortex = showwaitfortex
1000 if texipc is not None:
1001 self.texipc = texipc
1002 if texdebug is not None:
1003 if self.texdebug is not None:
1004 self.texdebug.close()
1005 if texdebug[-4:] == ".tex":
1006 self.texdebug = open(texdebug, "w")
1007 else:
1008 self.texdebug = open("%s.tex" % texdebug, "w")
1009 if dvidebug is not None:
1010 self.dvidebug = dvidebug
1011 if errordebug is not None:
1012 self.errordebug = errordebug
1013 if dvicopy is not None:
1014 self.dvicopy = dvicopy
1015 if pyxgraphics is not None:
1016 self.pyxgraphics = pyxgraphics
1017 if errordebug is not None:
1018 self.errordebug = errordebug
1019 if texmessagestart is not None:
1020 texmessagestart = helper.ensuresequence(texmessagestart)
1021 helper.checkattr(texmessagestart, allowmulti=(texmessage,))
1022 self.texmessagestart = texmessagestart
1023 if texmessagedocclass is not None:
1024 texmessagedocclass = helper.ensuresequence(texmessagedocclass)
1025 helper.checkattr(texmessagedocclass, allowmulti=(texmessage,))
1026 self.texmessagedocclass = texmessagedocclass
1027 if texmessagebegindoc is not None:
1028 texmessagebegindoc = helper.ensuresequence(texmessagebegindoc)
1029 helper.checkattr(texmessagebegindoc, allowmulti=(texmessage,))
1030 self.texmessagebegindoc = texmessagebegindoc
1031 if texmessageend is not None:
1032 texmessageend = helper.ensuresequence(texmessageend)
1033 helper.checkattr(texmessageend, allowmulti=(texmessage,))
1034 self.texmessageend = texmessageend
1035 if texmessagedefaultpreamble is not None:
1036 texmessagedefaultpreamble = helper.ensuresequence(texmessagedefaultpreamble)
1037 helper.checkattr(texmessagedefaultpreamble, allowmulti=(texmessage,))
1038 self.texmessagedefaultpreamble = texmessagedefaultpreamble
1039 if texmessagedefaultrun is not None:
1040 texmessagedefaultrun = helper.ensuresequence(texmessagedefaultrun)
1041 helper.checkattr(texmessagedefaultrun, allowmulti=(texmessage,))
1042 self.texmessagedefaultrun = texmessagedefaultrun
1044 def preamble(self, expr, *args):
1045 r"""put something into the TeX/LaTeX preamble
1046 - in LaTeX, this is done before the \begin{document}
1047 (you might use \AtBeginDocument, when you're in need for)
1048 - it is not allowed to call preamble after calling the
1049 text method for the first time (for LaTeX this is needed
1050 due to \begin{document}; in TeX it is forced for compatibility
1051 (you should be able to switch from TeX to LaTeX, if you want,
1052 without breaking something
1053 - preamble expressions must not create any dvi output
1054 - args might contain texmessage instances"""
1055 if self.texdone or not self.preamblemode:
1056 raise TexNotInPreambleModeError
1057 helper.checkattr(args, allowmulti=(texmessage,))
1058 args = helper.getattrs(args, texmessage, default=self.texmessagedefaultpreamble)
1059 self.execute(expr, *args)
1060 self.preambles.append((expr, args))
1062 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:")
1064 def text_pt(self, x, y, expr, *args):
1065 """create text by passing expr to TeX/LaTeX
1066 - returns a textbox containing the result from running expr thru TeX/LaTeX
1067 - the box center is set to x, y
1068 - *args may contain attr parameters, namely:
1069 - textattr instances
1070 - texmessage instances
1071 - trafo._trafo instances
1072 - style.fillstyle instances"""
1073 if expr is None:
1074 raise ValueError("None expression is invalid")
1075 if self.texdone:
1076 self.reset(reinit=1)
1077 first = 0
1078 if self.preamblemode:
1079 if self.mode == "latex":
1080 self.execute("\\begin{document}", *self.texmessagebegindoc)
1081 self.preamblemode = 0
1082 first = 1
1083 if self.texipc and self.dvicopy:
1084 raise RuntimeError("texipc and dvicopy can't be mixed up")
1085 helper.checkattr(args, allowmulti=(textattr, texmessage, trafo._trafo, style.fillstyle))
1086 textattrs = attr.getattrs(args, [textattr])
1087 textattrs = attr.mergeattrs(textattrs)
1088 lentextattrs = len(textattrs)
1089 for i in range(lentextattrs):
1090 expr = textattrs[lentextattrs-1-i].apply(expr)
1091 self.execute(expr, *helper.getattrs(args, texmessage, default=self.texmessagedefaultrun))
1092 if self.texipc:
1093 if first:
1094 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1095 match = self.PyXBoxPattern.search(self.texmessage)
1096 if not match or int(match.group("page")) != self.page:
1097 raise TexResultError("box extents not found", self)
1098 left, right, height, depth = map(lambda x: float(x) * 72.0 / 72.27, match.group("lt", "rt", "ht", "dp"))
1099 box = textbox_pt(x, y, left, right, height, depth, self.finishdvi,
1100 *helper.getattrs(args, style.fillstyle, default=[]))
1101 for t in helper.getattrs(args, trafo._trafo, default=()):
1102 box.reltransform(t)
1103 if self.texipc:
1104 box.setdvicanvas(self.dvifile.readpage())
1105 self.acttextboxes.append(box)
1106 return box
1108 def text(self, x, y, expr, *args):
1109 return self.text_pt(unit.topt(x), unit.topt(y), expr, *args)
1112 # the module provides an default texrunner and methods for direct access
1113 defaulttexrunner = texrunner()
1114 reset = defaulttexrunner.reset
1115 set = defaulttexrunner.set
1116 preamble = defaulttexrunner.preamble
1117 text = defaulttexrunner.text
1118 text_pt = defaulttexrunner.text_pt