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 ###############################################################################
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
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(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
)
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
, 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
]
377 raise IndexError("index out of sizelist range")
381 def apply(self
, expr
):
382 return r
"\%s{%s}" % (self
.size
, expr
)
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)
396 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
398 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
404 def __init__(self
, width
, baseline
=top
):
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
)
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
):
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
):
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 ###############################################################################
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)
503 self
.expectqueue
= expectqueue
504 self
.gotevent
= gotevent
505 self
.gotqueue
= gotqueue
506 self
.quitevent
= quitevent
512 read
= self
.pipe
.readline() # read, what comes in
514 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
518 # universal EOL handling (convert everything into unix like EOLs)
519 read
.replace("\r", "")
520 if not len(read
) or read
[-1] != "\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
527 self
.expect
= self
.expectqueue
.get_nowait()
532 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
533 raise RuntimeError("TeX/LaTeX finished unexpectedly")
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
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
)
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:
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
])
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")
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(".")
616 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
619 for file in glob
.glob("%s.*" % texrunner
.texfilename
):
624 if texrunner
.texdebug
is not None:
626 texrunner
.texdebug
.close()
627 texrunner
.texdebug
= None
633 """TeX/LaTeX interface
634 - runs TeX/LaTeX expressions instantly
635 - checks TeX/LaTeX response
636 - the instance variable texmessage stores the last TeX
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
642 - the instance variable errordebug controls the verbose
643 level of TexResultError"""
645 def __init__(self
, mode
="tex",
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),
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
]):
666 if mode
!= "tex" and mode
!= "latex":
667 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
670 self
.docclass
= docclass
672 self
.usefiles
= usefiles
673 self
.fontmap
= dvifile
.readfontmap(fontmaps
.split())
674 self
.waitfortex
= waitfortex
675 self
.showwaitfortex
= showwaitfortex
677 if texdebug
is not None:
678 if texdebug
[-4:] == ".tex":
679 self
.texdebug
= open(texdebug
, "w")
681 self
.texdebug
= open("%s.tex" % texdebug
, "w")
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
697 self
.preamblemode
= 1
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
:
716 while waited
< self
.waitfortex
and not hasevent
:
717 if self
.waitfortex
- waited
> self
.showwaitfortex
:
718 event
.wait(self
.showwaitfortex
)
719 waited
+= self
.showwaitfortex
721 event
.wait(self
.waitfortex
- waited
)
722 waited
+= self
.waitfortex
- waited
723 hasevent
= event
.isSet()
725 if waited
< self
.waitfortex
:
726 sys
.stderr
.write("*** PyX INFO: still waiting for %s after %i seconds...\n" % (self
.mode
, waited
))
728 sys
.stderr
.write("*** PyX ERROR: the timeout of %i seconds expired and %s did not respond.\n" % (waited
, self
.mode
))
731 event
.wait(self
.waitfortex
)
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"""
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(".")
753 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
756 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
757 texfile
.write("\\relax%\n")
764 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
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
)
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":
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
)]:
812 lfsfile
= open(fulllfsname
, "r")
813 lfsdef
= lfsfile
.read()
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")))
826 lfsnames
.append(os
.path
.basename(f
)[:-4])
831 raise IOError("file '%s' is not available or not readable. Available LaTeX font size files (*.lfs): %s" % (lfsname
, lfsnames
))
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":
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")]:
843 open(pyxdef
, "r").close()
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"
858 if self
.docopt
is not None:
859 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
860 attr
.mergeattrs(self
.texmessagesdocclass
))
862 self
.execute("\\documentclass{%s}" % self
.docclass
,
863 attr
.mergeattrs(self
.texmessagesdocclass
))
864 self
.preamblemode
= oldpreamblemode
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
)
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"
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
889 self
.texinput
.close() # close the input queue and
890 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
894 self
.texmessage
+= self
.gotqueue
.get_nowait()
897 self
.texmessageparsed
= self
.texmessage
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
:
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
)
913 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
916 """finish TeX/LaTeX and read the dvifile
917 - this method ensures that all textboxes can access their
919 self
.execute(None, self
.texmessagesend
)
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
924 dvifilename
= "%s.dvi" % self
.texfilename
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")
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))"
938 if self
.texdebug
is not None:
939 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
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
952 self
.preamblemode
= 1
954 def set(self
, mode
=None,
968 texmessagesstart
=None,
969 texmessagesdocclass
=None,
970 texmessagesbegindoc
=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"""
979 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
982 if mode
!= "tex" and mode
!= "latex":
983 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
987 if docclass
is not None:
988 self
.docclass
= docclass
989 if docopt
is not None:
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")
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"""
1060 raise ValueError("None expression is invalid")
1062 self
.reset(reinit
=1)
1064 if self
.preamblemode
:
1065 if self
.mode
== "latex":
1066 self
.execute("\\begin{document}", self
.texmessagesbegindoc
)
1067 self
.preamblemode
= 0
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
))
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
)
1091 box
.setdvicanvas(self
.dvifile
.readpage())
1092 self
.acttextboxes
.append(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