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 glob
, os
, threading
, Queue
, traceback
, re
, tempfile
, sys
, atexit
, time
26 import config
, helper
, unit
, box
, canvas
, trafo
, version
, attr
, style
, dvifile
28 ###############################################################################
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
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())
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"""
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
)
108 raise TexResultError("TeX startup failed", texrunner
)
109 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[m
.end():]
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
)
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
):
127 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.aux." % texrunner
.texfilename
, 1)
128 texrunner
.texmessageparsed
= s1
+ s2
129 except (IndexError, ValueError):
131 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.aux." % (os
.curdir
,
133 texrunner
.texfilename
), 1)
134 texrunner
.texmessageparsed
= s1
+ s2
135 except (IndexError, ValueError):
139 class _texmessageinputmarker(texmessage
):
140 """validates the PyXInputMarker"""
142 __implements__
= _Itexmessage
144 def check(self
, texrunner
):
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():]
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
):
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
):
187 s1
, s2
= texrunner
.texmessageparsed
.split("(%s.aux)" % texrunner
.texfilename
, 1)
188 texrunner
.texmessageparsed
= s1
+ s2
189 except (IndexError, ValueError):
191 s1
, s2
= texrunner
.texmessageparsed
.split("(%s%s%s.aux)" % (os
.curdir
,
193 texrunner
.texfilename
), 1)
194 texrunner
.texmessageparsed
= s1
+ s2
195 except (IndexError, ValueError):
198 s1
, s2
= texrunner
.texmessageparsed
.split("(see the transcript file for additional information)", 1)
199 texrunner
.texmessageparsed
= s1
+ s2
200 except (IndexError, ValueError):
202 dvipattern
= re
.compile(r
"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner
.texfilename
)
203 m
= dvipattern
.search(texrunner
.texmessageparsed
)
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():]
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
)
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
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"""
258 if level
> highestlevel
:
260 if level
<= maxlevel
:
264 if level
== 0 and highestlevel
> 0:
267 def check(self
, texrunner
):
268 lowestbracketlevel
= self
.baselevels(texrunner
.texmessageparsed
)
269 if lowestbracketlevel
is not None:
270 m
= self
.pattern
.search(lowestbracketlevel
)
272 if os
.access(m
.group("filename"), os
.R_OK
):
273 lowestbracketlevel
= lowestbracketlevel
[:m
.start()] + lowestbracketlevel
[m
.end():]
276 m
= self
.pattern
.search(lowestbracketlevel
)
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
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 ###############################################################################
328 ###############################################################################
330 _textattrspreamble
= ""
333 "a textattr defines a apply method, which modifies a (La)TeX expression"
335 class halign(attr
.exclusiveattr
, textattr
):
337 def __init__(self
, 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
):
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
):
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
]
375 raise IndexError("index out of sizelist range")
379 def apply(self
, expr
):
380 return r
"\%s{%s}" % (self
.size
, expr
)
382 for s
in defaultsizelist
:
384 setattr(size
, s
, size(s
))
387 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
389 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
395 def __init__(self
, width
, baseline
=top
):
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
)
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
):
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
):
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 ###############################################################################
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)
494 self
.expectqueue
= expectqueue
495 self
.gotevent
= gotevent
496 self
.gotqueue
= gotqueue
497 self
.quitevent
= quitevent
503 read
= self
.pipe
.readline() # read, what comes in
505 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
509 # universal EOL handling (convert everything into unix like EOLs)
510 read
.replace("\r", "")
511 if not len(read
) or read
[-1] != "\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
518 self
.expect
= self
.expectqueue
.get_nowait()
523 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
524 raise RuntimeError("TeX/LaTeX finished unexpectedly")
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
550 def transform(self
, *trafos
):
551 box
._rect
.transform(self
, *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:
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
])
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")
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(".")
602 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
605 for file in glob
.glob("%s.*" % texrunner
.texfilename
):
610 if texrunner
.texdebug
is not None:
612 texrunner
.texdebug
.close()
613 texrunner
.texdebug
= None
618 # texrunner state exceptions
619 class TexRunsError(Exception): pass
620 class TexDoneError(Exception): pass
621 class TexNotInPreambleModeError(Exception): pass
625 """TeX/LaTeX interface
626 - runs TeX/LaTeX expressions instantly
627 - checks TeX/LaTeX response
628 - the instance variable texmessage stores the last TeX
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
634 - the instance variable errordebug controls the verbose
635 level of TexResultError"""
637 def __init__(self
, mode
="tex",
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),
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
):
658 if mode
!= "tex" and mode
!= "latex":
659 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
662 self
.docclass
= docclass
664 self
.usefiles
= helper
.ensurelist(usefiles
)
665 self
.fontmap
= dvifile
.readfontmap(fontmaps
.split())
666 self
.waitfortex
= waitfortex
667 self
.showwaitfortex
= showwaitfortex
669 if texdebug
is not None:
670 if texdebug
[-4:] == ".tex":
671 self
.texdebug
= open(texdebug
, "w")
673 self
.texdebug
= open("%s.tex" % texdebug
, "w")
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
701 self
.preamblemode
= 1
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
:
720 while waited
< self
.waitfortex
and not hasevent
:
721 if self
.waitfortex
- waited
> self
.showwaitfortex
:
722 event
.wait(self
.showwaitfortex
)
723 waited
+= self
.showwaitfortex
725 event
.wait(self
.waitfortex
- waited
)
726 waited
+= self
.waitfortex
- waited
727 hasevent
= event
.isSet()
729 if waited
< self
.waitfortex
:
730 sys
.stderr
.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self
.mode
, waited
))
732 sys
.stderr
.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited
, self
.mode
))
735 event
.wait(self
.waitfortex
)
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
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(".")
757 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
760 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
761 texfile
.write("\\relax%\n")
768 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
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
)
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":
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
)]:
816 lfsfile
= open(fulllfsname
, "r")
817 lfsdef
= lfsfile
.read()
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")))
830 lfsnames
.append(os
.path
.basename(f
)[:-4])
835 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname
, lfsnames
))
837 raise IOError("file '%s' is not available or not readable. No LaTeX font size files (*.lfs) available. Check your installation." % lfsname
)
839 self
.execute("\\normalsize%\n")
840 self
.execute("\\newdimen\\linewidth%\n")
841 elif self
.mode
== "latex":
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")]:
847 open(pyxdef
, "r").close()
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"
861 if self
.docopt
is not None:
862 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
), *self
.texmessagedocclass
)
864 self
.execute("\\documentclass{%s}" % self
.docclass
, *self
.texmessagedocclass
)
865 self
.preamblemode
= oldpreamblemode
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
)
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"
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
890 self
.texinput
.close() # close the input queue and
891 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
895 self
.texmessage
+= self
.gotqueue
.get_nowait()
898 self
.texmessageparsed
= self
.texmessage
901 texmessage
.inputmarker
.check(self
)
902 if not self
.preamblemode
:
903 texmessage
.pyxbox
.check(self
)
904 texmessage
.pyxpageout
.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
)
914 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
917 """finish TeX/LaTeX and read the dvifile
918 - this method ensures that all textboxes can access their
920 self
.execute(None, *self
.texmessageend
)
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
925 dvifilename
= "%s.dvi" % self
.texfilename
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")
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))"
939 if self
.texdebug
is not None:
940 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
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
953 self
.preamblemode
= 1
955 def set(self
, mode
=None,
969 texmessagestart
=None,
970 texmessagedocclass
=None,
971 texmessagebegindoc
=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"""
983 if mode
!= "tex" and mode
!= "latex":
984 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
988 if docclass
is not None:
989 self
.docclass
= docclass
990 if docopt
is not None:
992 if usefiles
is not None:
993 self
.usefiles
= helper
.ensurelist(usefiles
)
994 if fontmaps
is not None:
995 self
.fontmap
= dvifile
.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")
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"""
1074 raise ValueError("None expression is invalid")
1076 self
.reset(reinit
=1)
1078 if self
.preamblemode
:
1079 if self
.mode
== "latex":
1080 self
.execute("\\begin{document}", *self
.texmessagebegindoc
)
1081 self
.preamblemode
= 0
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
))
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
=()):
1104 box
.setdvicanvas(self
.dvifile
.readpage())
1105 self
.acttextboxes
.append(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