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-2005 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 import glob
, os
, threading
, Queue
, re
, tempfile
, atexit
, time
, warnings
26 import config
, siteconfig
, unit
, box
, canvas
, trafo
, version
, attr
, style
, dvifile
27 import bbox
as bboxmodule
29 ###############################################################################
31 # - please don't get confused:
32 # - there is a texmessage (and a texmessageparsed) attribute within the
33 # texrunner; it contains TeX/LaTeX response from the last command execution
34 # - instances of classes derived from the class texmessage are used to
35 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
36 # attribute of a texrunner instance
37 # - the multiple usage of the name texmessage might be removed in the future
38 # - texmessage instances should implement _Itexmessage
39 ###############################################################################
41 class TexResultError(RuntimeError):
42 """specialized texrunner exception class
43 - it is raised by texmessage instances, when a texmessage indicates an error
44 - it is raised by the texrunner itself, whenever there is a texmessage left
45 after all parsing of this message (by texmessage instances)
46 prints a detailed report about the problem
47 - the verbose level is controlled by texrunner.errordebug"""
49 def __init__(self
, description
, texrunner
):
50 if texrunner
.errordebug
>= 2:
51 self
.description
= ("%s\n" % description
+
52 "The expression passed to TeX was:\n"
53 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
54 "The return message from TeX was:\n"
55 " %s\n" % texrunner
.texmessage
.replace("\n", "\n ").rstrip() +
56 "After parsing this message, the following was left:\n"
57 " %s" % texrunner
.texmessageparsed
.replace("\n", "\n ").rstrip())
58 elif texrunner
.errordebug
== 1:
59 firstlines
= texrunner
.texmessageparsed
.split("\n")
60 if len(firstlines
) > 5:
61 firstlines
= firstlines
[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
62 self
.description
= ("%s\n" % description
+
63 "The expression passed to TeX was:\n"
64 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
65 "After parsing the return message from TeX, the following was left:\n" +
66 reduce(lambda x
, y
: "%s %s\n" % (x
,y
), firstlines
, "").rstrip())
68 self
.description
= description
71 return self
.description
75 """validates/invalidates TeX/LaTeX response"""
77 def check(self
, texrunner
):
78 """check a Tex/LaTeX response and respond appropriate
79 - read the texrunners texmessageparsed attribute
80 - if there is an problem found, raise TexResultError
81 - remove any valid and identified TeX/LaTeX response
82 from the texrunners texmessageparsed attribute
83 -> finally, there should be nothing left in there,
84 otherwise it is interpreted as an error"""
87 class texmessage(attr
.attr
): pass
90 class _texmessagestart(texmessage
):
91 """validates TeX/LaTeX startup"""
93 __implements__
= _Itexmessage
95 startpattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
97 def check(self
, texrunner
):
98 # check for "This is e-TeX"
99 m
= self
.startpattern
.search(texrunner
.texmessageparsed
)
101 raise TexResultError("TeX startup failed", texrunner
)
102 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[m
.end():]
104 # check for filename to be processed
106 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.split("%s.tex" % texrunner
.texfilename
, 1)[1]
107 except (IndexError, ValueError):
108 raise TexResultError("TeX running startup file failed", texrunner
)
110 # check for \raiseerror -- just to be sure that communication works
112 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
113 except (IndexError, ValueError):
114 raise TexResultError("TeX scrollmode check failed", texrunner
)
117 class _texmessagenoaux(texmessage
):
118 """allows for LaTeXs no-aux-file warning"""
120 __implements__
= _Itexmessage
122 def check(self
, texrunner
):
124 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.aux." % texrunner
.texfilename
, 1)
125 texrunner
.texmessageparsed
= s1
+ s2
126 except (IndexError, ValueError):
128 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.aux." % (os
.curdir
,
130 texrunner
.texfilename
), 1)
131 texrunner
.texmessageparsed
= s1
+ s2
132 except (IndexError, ValueError):
136 class _texmessageinputmarker(texmessage
):
137 """validates the PyXInputMarker"""
139 __implements__
= _Itexmessage
141 def check(self
, texrunner
):
143 s1
, s2
= texrunner
.texmessageparsed
.split("PyXInputMarker:executeid=%s:" % texrunner
.executeid
, 1)
144 texrunner
.texmessageparsed
= s1
+ s2
145 except (IndexError, ValueError):
146 raise TexResultError("PyXInputMarker expected", texrunner
)
149 class _texmessagepyxbox(texmessage
):
150 """validates the PyXBox output"""
152 __implements__
= _Itexmessage
154 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:")
156 def check(self
, texrunner
):
157 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
158 if m
and m
.group("page") == str(texrunner
.page
):
159 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
161 raise TexResultError("PyXBox expected", texrunner
)
164 class _texmessagepyxpageout(texmessage
):
165 """validates the dvi shipout message (writing a page to the dvi file)"""
167 __implements__
= _Itexmessage
169 def check(self
, texrunner
):
171 s1
, s2
= texrunner
.texmessageparsed
.split("[80.121.88.%s]" % texrunner
.page
, 1)
172 texrunner
.texmessageparsed
= s1
+ s2
173 except (IndexError, ValueError):
174 raise TexResultError("PyXPageOutMarker expected", texrunner
)
177 class _texmessageend(texmessage
):
178 """validates TeX/LaTeX finish"""
180 __implements__
= _Itexmessage
182 def check(self
, texrunner
):
184 s1
, s2
= texrunner
.texmessageparsed
.split("(%s.aux)" % texrunner
.texfilename
, 1)
185 texrunner
.texmessageparsed
= s1
+ s2
186 except (IndexError, ValueError):
188 s1
, s2
= texrunner
.texmessageparsed
.split("(%s%s%s.aux)" % (os
.curdir
,
190 texrunner
.texfilename
), 1)
191 texrunner
.texmessageparsed
= s1
+ s2
192 except (IndexError, ValueError):
195 # check for "(see the transcript file for additional information)"
197 s1
, s2
= texrunner
.texmessageparsed
.split("(see the transcript file for additional information)", 1)
198 texrunner
.texmessageparsed
= s1
+ s2
199 except (IndexError, ValueError):
202 # check for "Output written on ...dvi (1 page, 220 bytes)."
203 dvipattern
= re
.compile(r
"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner
.texfilename
)
204 m
= dvipattern
.search(texrunner
.texmessageparsed
)
207 raise TexResultError("TeX dvifile messages expected", texrunner
)
208 if m
.group("page") != str(texrunner
.page
):
209 raise TexResultError("wrong number of pages reported", texrunner
)
210 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
213 s1
, s2
= texrunner
.texmessageparsed
.split("No pages of output.", 1)
214 texrunner
.texmessageparsed
= s1
+ s2
215 except (IndexError, ValueError):
216 raise TexResultError("no dvifile expected", texrunner
)
218 # check for "Transcript written on ...log."
220 s1
, s2
= texrunner
.texmessageparsed
.split("Transcript written on %s.log." % texrunner
.texfilename
, 1)
221 texrunner
.texmessageparsed
= s1
+ s2
222 except (IndexError, ValueError):
223 raise TexResultError("TeX logfile message expected", texrunner
)
226 class _texmessageemptylines(texmessage
):
227 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
228 also clear TeX interactive mode warning (Please type a command or say `\\end')
231 __implements__
= _Itexmessage
233 def check(self
, texrunner
):
234 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace(r
"(Please type a command or say `\end')", "")
235 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace(" ", "")
236 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace("*\n", "")
237 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace("\n", "")
240 class _texmessageload(texmessage
):
241 """validates inclusion of arbitrary files
242 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
243 <fielname> is a readable file and other stuff can be anything
244 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
245 - this is not always wanted, but we just assume that file inclusion is fine"""
247 __implements__
= _Itexmessage
249 pattern
= re
.compile(r
"\((?P<filename>[^()\s\n]+)(?P<additional>[^()]*)\)")
251 def baselevels(self
, s
, maxlevel
=1, brackets
="()"):
252 """strip parts of a string above a given bracket level
253 - return a modified (some parts might be removed) version of the string s
254 where all parts inside brackets with level higher than maxlevel are
256 - if brackets do not match (number of left and right brackets is wrong
257 or at some points there were more right brackets than left brackets)
258 just return the unmodified string"""
265 if level
> highestlevel
:
267 if level
<= maxlevel
:
271 if level
== 0 and highestlevel
> 0:
274 def check(self
, texrunner
):
275 lowestbracketlevel
= self
.baselevels(texrunner
.texmessageparsed
)
276 if lowestbracketlevel
is not None:
277 m
= self
.pattern
.search(lowestbracketlevel
)
279 filename
= m
.group("filename").replace("\n", "")
281 additional
= m
.group("additional")
284 if (os
.access(filename
, os
.R_OK
) or
285 len(additional
) and additional
[0] == "\n" and os
.access(filename
+additional
.split()[0], os
.R_OK
)):
286 lowestbracketlevel
= lowestbracketlevel
[:m
.start()] + lowestbracketlevel
[m
.end():]
289 m
= self
.pattern
.search(lowestbracketlevel
)
291 texrunner
.texmessageparsed
= lowestbracketlevel
294 class _texmessageloaddef(_texmessageload
):
295 """validates the inclusion of font description files (fd-files)
296 - works like _texmessageload
297 - filename must end with .def or .fd and no further text is allowed"""
299 pattern
= re
.compile(r
"\((?P<filename>[^)]+(\.fd|\.def))\)")
301 def baselevels(self
, s
, **kwargs
):
305 class _texmessagegraphicsload(_texmessageload
):
306 """validates the inclusion of files as the graphics packages writes it
307 - works like _texmessageload, but using "<" and ">" as delimiters
308 - filename must end with .eps and no further text is allowed"""
310 pattern
= re
.compile(r
"<(?P<filename>[^>]+.eps)>")
312 def baselevels(self
, s
, **kwargs
):
316 class _texmessageignore(_texmessageload
):
317 """validates any TeX/LaTeX response
318 - this might be used, when the expression is ok, but no suitable texmessage
320 - PLEASE: - consider writing suitable tex message parsers
321 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
323 __implements__
= _Itexmessage
325 def check(self
, texrunner
):
326 texrunner
.texmessageparsed
= ""
329 texmessage
.start
= _texmessagestart()
330 texmessage
.noaux
= _texmessagenoaux()
331 texmessage
.end
= _texmessageend()
332 texmessage
.load
= _texmessageload()
333 texmessage
.loaddef
= _texmessageloaddef()
334 texmessage
.graphicsload
= _texmessagegraphicsload()
335 texmessage
.ignore
= _texmessageignore()
338 texmessage
.inputmarker
= _texmessageinputmarker()
339 texmessage
.pyxbox
= _texmessagepyxbox()
340 texmessage
.pyxpageout
= _texmessagepyxpageout()
341 texmessage
.emptylines
= _texmessageemptylines()
344 class _texmessageallwarning(texmessage
):
345 """validates a given pattern 'pattern' as a warning 'warning'"""
347 def check(self
, texrunner
):
348 if texrunner
.texmessageparsed
:
349 warnings
.warn("ignoring all warnings:\n%s" % texrunner
.texmessageparsed
)
350 texrunner
.texmessageparsed
= ""
352 texmessage
.allwarning
= _texmessageallwarning()
355 class texmessagepattern(texmessage
):
356 """validates a given pattern and issue a warning (when set)"""
358 def __init__(self
, pattern
, warning
=None):
359 self
.pattern
= pattern
360 self
.warning
= warning
362 def check(self
, texrunner
):
363 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
365 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
367 warnings
.warn("%s:\n%s" % (self
.warning
, m
.string
[m
.start(): m
.end()].rstrip()))
368 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
370 texmessage
.fontwarning
= texmessagepattern(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
), "ignoring font warning")
371 texmessage
.boxwarning
= texmessagepattern(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
), "ignoring overfull/underfull box warning")
375 ###############################################################################
377 ###############################################################################
379 _textattrspreamble
= ""
382 "a textattr defines a apply method, which modifies a (La)TeX expression"
384 class _localattr
: pass
386 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
387 \newdimen\PyXraggedskipplus%
388 \def\PyXragged{\PyXraggedskipplus=4em%
389 \leftskip=0pt plus \PyXFlushHAlign\PyXraggedskipplus%
390 \rightskip=0pt plus \PyXraggedskipplus%
391 \advance\rightskip0pt plus -\PyXFlushHAlign\PyXraggedskipplus%
399 \exhyphenpenalty=9999}%
402 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
404 def __init__(self
, aboxhalign
):
405 self
.boxhalign
= aboxhalign
406 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
408 def apply(self
, expr
):
409 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
411 boxhalign
.left
= boxhalign(0)
412 boxhalign
.center
= boxhalign(0.5)
413 boxhalign
.right
= boxhalign(1)
414 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
417 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
419 def __init__(self
, aflushhalign
):
420 self
.flushhalign
= aflushhalign
421 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
423 def apply(self
, expr
):
424 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
426 flushhalign
.left
= flushhalign(0)
427 flushhalign
.center
= flushhalign(0.5)
428 flushhalign
.right
= flushhalign(1)
429 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
432 class halign(attr
.exclusiveattr
, textattr
, boxhalign
, flushhalign
, _localattr
):
434 def __init__(self
, aboxhalign
, aflushhalign
):
435 self
.boxhalign
= aboxhalign
436 self
.flushhalign
= aflushhalign
437 attr
.exclusiveattr
.__init
__(self
, halign
)
439 def apply(self
, expr
):
440 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
442 halign
.left
= halign(0, 0)
443 halign
.center
= halign(0.5, 0.5)
444 halign
.right
= halign(1, 1)
445 halign
.clear
= attr
.clearclass(halign
)
446 halign
.boxleft
= boxhalign
.left
447 halign
.boxcenter
= boxhalign
.center
448 halign
.boxright
= boxhalign
.right
449 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
450 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
451 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
454 class _mathmode(attr
.attr
, textattr
, _localattr
):
457 def apply(self
, expr
):
458 return r
"$\displaystyle{%s}$" % expr
460 mathmode
= _mathmode()
461 clearmathmode
= attr
.clearclass(_mathmode
)
464 class _phantom(attr
.attr
, textattr
, _localattr
):
467 def apply(self
, expr
):
468 return r
"\phantom{%s}" % expr
471 clearphantom
= attr
.clearclass(_phantom
)
474 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
476 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
482 def __init__(self
, width
, baseline
=top
):
483 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
484 self
.baseline
= baseline
485 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
487 def apply(self
, expr
):
488 if self
.baseline
== self
.top
:
489 return r
"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self
.width
, expr
)
490 elif self
.baseline
== self
.middle
:
491 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
)
492 elif self
.baseline
== self
.bottom
:
493 return r
"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self
.width
, expr
)
495 RuntimeError("invalid baseline argument")
497 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
499 class parbox(parbox_pt
):
501 def __init__(self
, width
, **kwargs
):
502 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
504 parbox
.clear
= parbox_pt
.clear
507 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
509 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
511 def __init__(self
, avalign
):
512 self
.valign
= avalign
513 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
515 def apply(self
, expr
):
516 return r
"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr
, 1-self
.valign
, self
.valign
)
518 valign
.top
= valign(0)
519 valign
.middle
= valign(0.5)
520 valign
.bottom
= valign(1)
521 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
524 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
526 class _vshift(attr
.sortbeforeattr
, textattr
):
529 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
531 def apply(self
, expr
):
532 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
534 class vshift(_vshift
):
535 "vertical down shift by a fraction of a character height"
537 def __init__(self
, lowerratio
, heightstr
="0"):
538 _vshift
.__init
__(self
)
539 self
.lowerratio
= lowerratio
540 self
.heightstr
= heightstr
542 def setheightexpr(self
):
543 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
545 class _vshiftmathaxis(_vshift
):
546 "vertical down shift by the height of the math axis"
548 def setheightexpr(self
):
549 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
552 vshift
.bottomzero
= vshift(0)
553 vshift
.middlezero
= vshift(0.5)
554 vshift
.topzero
= vshift(1)
555 vshift
.mathaxis
= _vshiftmathaxis()
556 vshift
.clear
= attr
.clearclass(_vshift
)
559 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
560 None, "tiny", "scriptsize", "footnotesize", "small"]
562 class size(attr
.sortbeforeattr
, textattr
):
565 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
566 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
567 raise RuntimeError("either specify sizeindex or sizename")
568 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
569 if sizeindex
is not None:
570 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
571 self
.size
= sizelist
[sizeindex
]
572 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
573 self
.size
= sizelist
[sizeindex
]
575 raise IndexError("index out of sizelist range")
579 def apply(self
, expr
):
580 return r
"\%s{}%s" % (self
.size
, expr
)
583 size
.scriptsize
= size
.script
= size(-3)
584 size
.footnotesize
= size
.footnote
= size(-2)
585 size
.small
= size(-1)
586 size
.normalsize
= size
.normal
= size(0)
592 size
.clear
= attr
.clearclass(size
)
595 ###############################################################################
597 ###############################################################################
600 class _readpipe(threading
.Thread
):
601 """threaded reader of TeX/LaTeX output
602 - sets an event, when a specific string in the programs output is found
603 - sets an event, when the terminal ends"""
605 def __init__(self
, pipe
, expectqueue
, gotevent
, gotqueue
, quitevent
):
606 """initialize the reader
607 - pipe: file to be read from
608 - expectqueue: keeps the next InputMarker to be wait for
609 - gotevent: the "got InputMarker" event
610 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
611 - quitevent: the "end of terminal" event"""
612 threading
.Thread
.__init
__(self
)
613 self
.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
615 self
.expectqueue
= expectqueue
616 self
.gotevent
= gotevent
617 self
.gotqueue
= gotqueue
618 self
.quitevent
= quitevent
624 read
= self
.pipe
.readline() # read, what comes in
626 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
630 # universal EOL handling (convert everything into unix like EOLs)
631 # XXX is this necessary on pipes?
632 read
= read
.replace("\r", "").replace("\n", "") + "\n"
633 self
.gotqueue
.put(read
) # report, whats read
634 if self
.expect
is not None and read
.find(self
.expect
) != -1:
635 self
.gotevent
.set() # raise the got event, when the output was expected (XXX: within a single line)
636 read
= self
.pipe
.readline() # read again
638 self
.expect
= self
.expectqueue
.get_nowait()
643 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
644 raise RuntimeError("TeX/LaTeX finished unexpectedly")
648 class textbox(box
.rect
, canvas
._canvas
):
649 """basically a box.rect, but it contains a text created by the texrunner
650 - texrunner._text and texrunner.text return such an object
651 - _textbox instances can be inserted into a canvas
652 - the output is contained in a page of the dvifile available thru the texrunner"""
653 # TODO: shouldn't all boxes become canvases? how about inserts then?
655 def __init__(self
, x
, y
, left
, right
, height
, depth
, finishdvi
, attrs
):
657 - finishdvi is a method to be called to get the dvicanvas
658 (e.g. the finishdvi calls the setdvicanvas method)
659 - attrs are fillstyles"""
662 self
.width
= left
+ right
665 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
666 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
667 canvas
._canvas
.__init
__(self
)
668 self
.finishdvi
= finishdvi
669 self
.dvicanvas
= None
671 self
.insertdvicanvas
= 0
673 def transform(self
, *trafos
):
674 if self
.insertdvicanvas
:
675 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
676 box
.rect
.transform(self
, *trafos
)
678 self
.texttrafo
= trafo
* self
.texttrafo
680 def setdvicanvas(self
, dvicanvas
):
681 if self
.dvicanvas
is not None:
682 raise RuntimeError("multiple call to setdvicanvas")
683 self
.dvicanvas
= dvicanvas
685 def ensuredvicanvas(self
):
686 if self
.dvicanvas
is None:
688 assert self
.dvicanvas
is not None, "finishdvi is broken"
689 if not self
.insertdvicanvas
:
690 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
691 self
.insertdvicanvas
= 1
693 def marker(self
, marker
):
694 self
.ensuredvicanvas()
695 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
697 def processPS(self
, file, writer
, context
, registry
, bbox
):
698 self
.ensuredvicanvas()
699 abbox
= bboxmodule
.empty()
700 canvas
._canvas
.processPS(self
, file, writer
, context
, registry
, abbox
)
701 bbox
+= box
.rect
.bbox(self
)
703 def processPDF(self
, file, writer
, context
, registry
, bbox
):
704 self
.ensuredvicanvas()
705 abbox
= bboxmodule
.empty()
706 canvas
._canvas
.processPDF(self
, file, writer
, context
, registry
, abbox
)
707 bbox
+= box
.rect
.bbox(self
)
710 def _cleantmp(texrunner
):
711 """get rid of temporary files
712 - function to be registered by atexit
713 - files contained in usefiles are kept"""
714 if texrunner
.texruns
: # cleanup while TeX is still running?
715 texrunner
.expectqueue
.put_nowait(None) # do not expect any output anymore
716 if texrunner
.mode
== "latex": # try to immediately quit from TeX or LaTeX
717 texrunner
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
719 texrunner
.texinput
.write("\n\\end\n")
720 texrunner
.texinput
.close() # close the input queue and
721 if not texrunner
.waitforevent(texrunner
.quitevent
): # wait for finish of the output
722 return # didn't got a quit from TeX -> we can't do much more
723 texrunner
.texruns
= 0
724 texrunner
.texdone
= 1
725 for usefile
in texrunner
.usefiles
:
726 extpos
= usefile
.rfind(".")
728 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
731 for file in glob
.glob("%s.*" % texrunner
.texfilename
):
736 if texrunner
.texdebug
is not None:
738 texrunner
.texdebug
.close()
739 texrunner
.texdebug
= None
748 """TeX/LaTeX interface
749 - runs TeX/LaTeX expressions instantly
750 - checks TeX/LaTeX response
751 - the instance variable texmessage stores the last TeX
753 - the instance variable texmessageparsed stores a parsed
754 version of texmessage; it should be empty after
755 texmessage.check was called, otherwise a TexResultError
757 - the instance variable errordebug controls the verbose
758 level of TexResultError"""
760 defaulttexmessagesstart
= [texmessage
.start
]
761 defaulttexmessagesdocclass
= [texmessage
.load
]
762 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.noaux
]
763 defaulttexmessagesend
= [texmessage
.end
, texmessage
.fontwarning
]
764 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
765 defaulttexmessagesdefaultrun
= [texmessage
.loaddef
, texmessage
.graphicsload
,
766 texmessage
.fontwarning
, texmessage
.boxwarning
]
768 def __init__(self
, mode
="tex",
773 fontmaps
=config
.get("text", "fontmaps", "psfonts.map"),
774 waitfortex
=config
.getint("text", "waitfortex", 60),
775 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
776 texipc
=config
.getboolean("text", "texipc", 0),
782 texmessagesdocclass
=[],
783 texmessagesbegindoc
=[],
785 texmessagesdefaultpreamble
=[],
786 texmessagesdefaultrun
=[]):
788 if mode
!= "tex" and mode
!= "latex":
789 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
792 self
.docclass
= docclass
794 self
.usefiles
= usefiles
795 self
.fontmaps
= fontmaps
796 self
.waitfortex
= waitfortex
797 self
.showwaitfortex
= showwaitfortex
799 if texdebug
is not None:
800 if texdebug
[-4:] == ".tex":
801 self
.texdebug
= open(texdebug
, "w")
803 self
.texdebug
= open("%s.tex" % texdebug
, "w")
806 self
.dvidebug
= dvidebug
807 self
.errordebug
= errordebug
808 self
.pyxgraphics
= pyxgraphics
809 self
.texmessagesstart
= texmessagesstart
810 self
.texmessagesdocclass
= texmessagesdocclass
811 self
.texmessagesbegindoc
= texmessagesbegindoc
812 self
.texmessagesend
= texmessagesend
813 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
814 self
.texmessagesdefaultrun
= texmessagesdefaultrun
818 self
.preamblemode
= 1
822 self
.needdvitextboxes
= [] # when texipc-mode off
824 self
.textboxesincluded
= 0
825 savetempdir
= tempfile
.tempdir
826 tempfile
.tempdir
= os
.curdir
827 self
.texfilename
= os
.path
.basename(tempfile
.mktemp())
828 tempfile
.tempdir
= savetempdir
830 def waitforevent(self
, event
):
831 """waits verbosely with an timeout for an event
832 - observes an event while periodly while printing messages
833 - returns the status of the event (isSet)
834 - does not clear the event"""
835 if self
.showwaitfortex
:
838 while waited
< self
.waitfortex
and not hasevent
:
839 if self
.waitfortex
- waited
> self
.showwaitfortex
:
840 event
.wait(self
.showwaitfortex
)
841 waited
+= self
.showwaitfortex
843 event
.wait(self
.waitfortex
- waited
)
844 waited
+= self
.waitfortex
- waited
845 hasevent
= event
.isSet()
847 if waited
< self
.waitfortex
:
848 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
850 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
853 event
.wait(self
.waitfortex
)
856 def execute(self
, expr
, texmessages
):
857 """executes expr within TeX/LaTeX
858 - if self.texruns is not yet set, TeX/LaTeX is initialized,
859 self.texruns is set and self.preamblemode is set
860 - the method must not be called, when self.texdone is already set
861 - expr should be a string or None
862 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
863 self.texdone becomes set
864 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
865 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
866 - texmessages is a list of texmessage instances"""
868 if self
.texdebug
is not None:
869 self
.texdebug
.write("%% PyX %s texdebug file\n" % version
.version
)
870 self
.texdebug
.write("%% mode: %s\n" % self
.mode
)
871 self
.texdebug
.write("%% date: %s\n" % time
.asctime(time
.localtime(time
.time())))
872 for usefile
in self
.usefiles
:
873 extpos
= usefile
.rfind(".")
875 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
878 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
879 texfile
.write("\\relax%\n")
886 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
888 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
889 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t")
890 atexit
.register(_cleantmp
, self
)
891 self
.expectqueue
= Queue
.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
892 self
.gotevent
= threading
.Event() # keeps the got inputmarker event
893 self
.gotqueue
= Queue
.Queue(0) # allow arbitrary number of entries
894 self
.quitevent
= threading
.Event() # keeps for end of terminal event
895 self
.readoutput
= _readpipe(self
.texoutput
, self
.expectqueue
, self
.gotevent
, self
.gotqueue
, self
.quitevent
)
897 self
.fontmap
= dvifile
.readfontmap(self
.fontmaps
.split())
898 oldpreamblemode
= self
.preamblemode
899 self
.preamblemode
= 1
900 self
.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
901 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
902 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
903 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
904 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
905 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
906 "\\newdimen\\PyXDimenHAlignRT%\n" +
907 _textattrspreamble
+ # insert preambles for textattrs macros
908 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
909 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
910 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
911 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
912 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
913 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
914 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
915 "lt=\\the\\PyXDimenHAlignLT,"
916 "rt=\\the\\PyXDimenHAlignRT,"
917 "ht=\\the\\ht\\PyXBox,"
918 "dp=\\the\\dp\\PyXBox:}%\n"
919 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
920 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
921 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
922 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
923 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
924 self
.defaulttexmessagesstart
+ self
.texmessagesstart
)
925 os
.remove("%s.tex" % self
.texfilename
)
926 if self
.mode
== "tex":
929 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
932 lfsname
= "%s.lfs" % self
.lfs
933 for fulllfsname
in [lfsname
,
934 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
936 lfsfile
= open(fulllfsname
, "r")
937 lfsdef
= lfsfile
.read()
943 lfserror
= "File '%s' is not available or not readable. " % lfsname
946 if lfserror
is not None:
947 allfiles
= (glob
.glob("*.lfs") +
948 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
953 lfsnames
.append(os
.path
.basename(f
)[:-4])
958 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
960 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror
)
961 self
.execute(lfsdef
, [])
962 self
.execute("\\normalsize%\n", [])
963 self
.execute("\\newdimen\\linewidth%\n", [])
964 elif self
.mode
== "latex":
966 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
968 open(pyxdef
, "r").close()
970 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
971 pyxdef
= os
.path
.abspath(pyxdef
).replace(os
.sep
, "/")
972 self
.execute("\\makeatletter%\n"
973 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
974 "\\def\\ProcessOptions{%\n"
975 "\\def\\Gin@driver{" + pyxdef
+ "}%\n"
976 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
977 "\\saveProcessOptions}%\n"
980 if self
.docopt
is not None:
981 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
982 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
984 self
.execute("\\documentclass{%s}" % self
.docclass
,
985 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
986 self
.preamblemode
= oldpreamblemode
988 if expr
is not None: # TeX/LaTeX should process expr
989 self
.expectqueue
.put_nowait("PyXInputMarker:executeid=%i:" % self
.executeid
)
990 if self
.preamblemode
:
991 self
.expr
= ("%s%%\n" % expr
+
992 "\\PyXInput{%i}%%\n" % self
.executeid
)
995 self
.expr
= ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr
, self
.page
) +
996 "\\PyXInput{%i}%%\n" % self
.executeid
)
997 else: # TeX/LaTeX should be finished
998 self
.expectqueue
.put_nowait("Transcript written on %s.log" % self
.texfilename
)
999 if self
.mode
== "latex":
1000 self
.expr
= "\\end{document}%\n"
1002 self
.expr
= "\\end%\n"
1003 if self
.texdebug
is not None:
1004 self
.texdebug
.write(self
.expr
)
1005 self
.texinput
.write(self
.expr
)
1006 gotevent
= self
.waitforevent(self
.gotevent
)
1007 self
.gotevent
.clear()
1008 if expr
is None and gotevent
: # TeX/LaTeX should have finished
1011 self
.texinput
.close() # close the input queue and
1012 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1014 self
.texmessage
= ""
1016 self
.texmessage
+= self
.gotqueue
.get_nowait()
1019 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1020 self
.texmessageparsed
= self
.texmessage
1022 if expr
is not None:
1023 texmessage
.inputmarker
.check(self
)
1024 if not self
.preamblemode
:
1025 texmessage
.pyxbox
.check(self
)
1026 texmessage
.pyxpageout
.check(self
)
1027 texmessages
= attr
.mergeattrs(texmessages
)
1028 for t
in texmessages
:
1030 keeptexmessageparsed
= self
.texmessageparsed
1031 texmessage
.emptylines
.check(self
)
1032 if len(self
.texmessageparsed
):
1033 self
.texmessageparsed
= keeptexmessageparsed
1034 raise TexResultError("unhandled TeX response (might be an error)", self
)
1036 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
1038 def finishdvi(self
, ignoretail
=0):
1039 """finish TeX/LaTeX and read the dvifile
1040 - this method ensures that all textboxes can access their
1042 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1043 dvifilename
= "%s.dvi" % self
.texfilename
1045 self
.dvifile
= dvifile
.dvifile(dvifilename
, self
.fontmap
, debug
=self
.dvidebug
)
1047 for box
in self
.needdvitextboxes
:
1048 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0]))
1050 if not ignoretail
and self
.dvifile
.readpage(None) is not None:
1051 raise RuntimeError("end of dvifile expected")
1053 self
.needdvitextboxes
= []
1055 def reset(self
, reinit
=0):
1056 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1059 if self
.texdebug
is not None:
1060 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
1065 self
.preamblemode
= 1
1066 for expr
, texmessages
in self
.preambles
:
1067 self
.execute(expr
, texmessages
)
1068 if self
.mode
== "latex":
1069 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1070 self
.preamblemode
= 0
1073 self
.preamblemode
= 1
1075 def set(self
, mode
=_unset
,
1082 showwaitfortex
=_unset
,
1088 texmessagesstart
=_unset
,
1089 texmessagesdocclass
=_unset
,
1090 texmessagesbegindoc
=_unset
,
1091 texmessagesend
=_unset
,
1092 texmessagesdefaultpreamble
=_unset
,
1093 texmessagesdefaultrun
=_unset
):
1094 """provide a set command for TeX/LaTeX settings
1095 - TeX/LaTeX must not yet been started
1096 - especially needed for the defaultrunner, where no access to
1097 the constructor is available"""
1099 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1100 if mode
is not _unset
:
1102 if mode
!= "tex" and mode
!= "latex":
1103 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1105 if lfs
is not _unset
:
1107 if docclass
is not _unset
:
1108 self
.docclass
= docclass
1109 if docopt
is not _unset
:
1110 self
.docopt
= docopt
1111 if usefiles
is not _unset
:
1112 self
.usefiles
= usefiles
1113 if fontmaps
is not _unset
:
1114 self
.fontmaps
= fontmaps
1115 if waitfortex
is not _unset
:
1116 self
.waitfortex
= waitfortex
1117 if showwaitfortex
is not _unset
:
1118 self
.showwaitfortex
= showwaitfortex
1119 if texipc
is not _unset
:
1120 self
.texipc
= texipc
1121 if texdebug
is not _unset
:
1122 if self
.texdebug
is not None:
1123 self
.texdebug
.close()
1124 if texdebug
[-4:] == ".tex":
1125 self
.texdebug
= open(texdebug
, "w")
1127 self
.texdebug
= open("%s.tex" % texdebug
, "w")
1128 if dvidebug
is not _unset
:
1129 self
.dvidebug
= dvidebug
1130 if errordebug
is not _unset
:
1131 self
.errordebug
= errordebug
1132 if pyxgraphics
is not _unset
:
1133 self
.pyxgraphics
= pyxgraphics
1134 if errordebug
is not _unset
:
1135 self
.errordebug
= errordebug
1136 if texmessagesstart
is not _unset
:
1137 self
.texmessagesstart
= texmessagesstart
1138 if texmessagesdocclass
is not _unset
:
1139 self
.texmessagesdocclass
= texmessagesdocclass
1140 if texmessagesbegindoc
is not _unset
:
1141 self
.texmessagesbegindoc
= texmessagesbegindoc
1142 if texmessagesend
is not _unset
:
1143 self
.texmessagesend
= texmessagesend
1144 if texmessagesdefaultpreamble
is not _unset
:
1145 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
1146 if texmessagesdefaultrun
is not _unset
:
1147 self
.texmessagesdefaultrun
= texmessagesdefaultrun
1149 def preamble(self
, expr
, texmessages
=[]):
1150 r
"""put something into the TeX/LaTeX preamble
1151 - in LaTeX, this is done before the \begin{document}
1152 (you might use \AtBeginDocument, when you're in need for)
1153 - it is not allowed to call preamble after calling the
1154 text method for the first time (for LaTeX this is needed
1155 due to \begin{document}; in TeX it is forced for compatibility
1156 (you should be able to switch from TeX to LaTeX, if you want,
1157 without breaking something)
1158 - preamble expressions must not create any dvi output
1159 - args might contain texmessage instances"""
1160 if self
.texdone
or not self
.preamblemode
:
1161 raise RuntimeError("preamble calls disabled due to previous text calls")
1162 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmessagesdefaultpreamble
+ texmessages
1163 self
.execute(expr
, texmessages
)
1164 self
.preambles
.append((expr
, texmessages
))
1166 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:")
1168 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[]):
1169 """create text by passing expr to TeX/LaTeX
1170 - returns a textbox containing the result from running expr thru TeX/LaTeX
1171 - the box center is set to x, y
1172 - *args may contain attr parameters, namely:
1173 - textattr instances
1174 - texmessage instances
1175 - trafo._trafo instances
1176 - style.fillstyle instances"""
1178 raise ValueError("None expression is invalid")
1180 self
.reset(reinit
=1)
1182 if self
.preamblemode
:
1183 if self
.mode
== "latex":
1184 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1185 self
.preamblemode
= 0
1187 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1188 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1189 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1190 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1191 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1192 # reverse loop over the merged textattrs (last is applied first)
1193 lentextattrs
= len(textattrs
)
1194 for i
in range(lentextattrs
):
1195 expr
= textattrs
[lentextattrs
-1-i
].apply(expr
)
1197 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1198 except TexResultError
:
1199 self
.finishdvi(ignoretail
=1)
1203 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1204 match
= self
.PyXBoxPattern
.search(self
.texmessage
)
1205 if not match
or int(match
.group("page")) != self
.page
:
1206 raise TexResultError("box extents not found", self
)
1207 left
, right
, height
, depth
= [float(xxx
)*72/72.27*unit
.x_pt
for xxx
in match
.group("lt", "rt", "ht", "dp")]
1208 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.finishdvi
, fillstyles
)
1212 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0]))
1214 self
.needdvitextboxes
.append(box
)
1217 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1218 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1220 PyXVariableBoxPattern
= re
.compile(r
"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1222 def textboxes(self
, text
, pageshapes
):
1223 # this is some experimental code to put text into several boxes
1224 # while the bounding shape changes from box to box (rectangles only)
1225 # first we load sev.tex
1226 if not self
.textboxesincluded
:
1227 self
.execute(r
"\input textboxes.tex", [texmessage
.load
])
1228 self
.textboxesincluded
= 1
1229 # define page shapes
1230 pageshapes_str
= "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit
.topt(pageshapes
[0][0]), 72.27/72*unit
.topt(pageshapes
[0][1]))
1231 pageshapes_str
+= "\\lohsizes={%\n"
1232 for hsize
, vsize
in pageshapes
[1:]:
1233 pageshapes_str
+= "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(hsize
))
1234 pageshapes_str
+= "{\\relax}%\n}%\n"
1235 pageshapes_str
+= "\\lovsizes={%\n"
1236 for hsize
, vsize
in pageshapes
[1:]:
1237 pageshapes_str
+= "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(vsize
))
1238 pageshapes_str
+= "{\\relax}%\n}%\n"
1244 self
.execute(pageshapes_str
, [])
1245 parnos_str
= "}{".join(parnos
)
1247 parnos_str
= "{%s}" % parnos_str
1248 parnos_str
= "\\parnos={%s{\\relax}}%%\n" % parnos_str
1249 self
.execute(parnos_str
, [])
1250 parshapes_str
= "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes
)
1251 self
.execute(parshapes_str
, [])
1252 self
.execute("\\global\\count0=1%%\n"
1253 "\\global\\parno=0%%\n"
1254 "\\global\\myprevgraf=0%%\n"
1255 "\\global\\showprevgraf=0%%\n"
1256 "\\global\\outputtype=0%%\n"
1257 "\\global\\leastcost=10000000%%\n"
1259 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1261 if self
.dvifile
is None:
1262 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1264 raise RuntimeError("textboxes currently needs texipc")
1267 lastparshapes
= parshapes
1270 lastpar
= prevgraf
= -1
1271 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
1274 page
= int(m
.group("page"))
1275 assert page
== pages
1276 par
= int(m
.group("par"))
1277 prevgraf
= int(m
.group("prevgraf"))
1278 if page
<= len(pageshapes
):
1279 width
= 72.27/72*unit
.topt(pageshapes
[page
-1][0])
1281 width
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1282 if page
< len(pageshapes
):
1283 nextwidth
= 72.27/72*unit
.topt(pageshapes
[page
][0])
1285 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1288 # a new paragraph is to be broken
1289 parnos
.append(str(par
))
1290 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1292 parshape
= " 0pt " + parshape
1293 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1294 elif prevgraf
== lastprevgraf
:
1297 # we have to append the breaking of the previous paragraph
1298 oldparshape
= " ".join(parshapes
[-1].split(' ')[2:2+2*lastprevgraf
])
1299 oldparshape
= oldparshape
.split('}')[0]
1301 oldparshape
= " " + oldparshape
1302 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1304 parshape
= " 0pt " + parshape
1307 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1309 lastprevgraf
= prevgraf
1311 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
1313 for i
in range(pages
):
1314 result
.append(self
.dvifile
.readpage([i
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1315 if parnos
== lastparnos
and parshapes
== lastparshapes
:
1319 raise TexResultError("Too many loops in textboxes ", texrunner
)
1322 # the module provides an default texrunner and methods for direct access
1323 defaulttexrunner
= texrunner()
1324 reset
= defaulttexrunner
.reset
1325 set = defaulttexrunner
.set
1326 preamble
= defaulttexrunner
.preamble
1327 text
= defaulttexrunner
.text
1328 text_pt
= defaulttexrunner
.text_pt
1330 def escapestring(s
, replace
={" ": "~",
1342 "\\": "{$\setminus$}",
1344 "escape all ascii characters such that they are printable by TeX/LaTeX"
1347 if not 32 <= ord(s
[i
]) < 127:
1348 raise ValueError("escapestring function handles ascii strings only")
1355 s
= s
[:i
] + r
+ s
[i
+1:]