2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004,2006 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}%
388 \leftskip=0pt plus \PyXFlushHAlign fil%
389 \rightskip=0pt plus 1fil%
390 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
396 \exhyphenpenalty=9999}%
399 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
401 def __init__(self
, aboxhalign
):
402 self
.boxhalign
= aboxhalign
403 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
405 def apply(self
, expr
):
406 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
408 boxhalign
.left
= boxhalign(0)
409 boxhalign
.center
= boxhalign(0.5)
410 boxhalign
.right
= boxhalign(1)
411 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
414 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
416 def __init__(self
, aflushhalign
):
417 self
.flushhalign
= aflushhalign
418 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
420 def apply(self
, expr
):
421 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
423 flushhalign
.left
= flushhalign(0)
424 flushhalign
.center
= flushhalign(0.5)
425 flushhalign
.right
= flushhalign(1)
426 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
429 class halign(attr
.exclusiveattr
, textattr
, boxhalign
, flushhalign
, _localattr
):
431 def __init__(self
, aboxhalign
, aflushhalign
):
432 self
.boxhalign
= aboxhalign
433 self
.flushhalign
= aflushhalign
434 attr
.exclusiveattr
.__init
__(self
, halign
)
436 def apply(self
, expr
):
437 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
439 halign
.left
= halign(0, 0)
440 halign
.center
= halign(0.5, 0.5)
441 halign
.right
= halign(1, 1)
442 halign
.clear
= attr
.clearclass(halign
)
443 halign
.boxleft
= boxhalign
.left
444 halign
.boxcenter
= boxhalign
.center
445 halign
.boxright
= boxhalign
.right
446 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
447 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
448 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
451 class _mathmode(attr
.attr
, textattr
, _localattr
):
454 def apply(self
, expr
):
455 return r
"$\displaystyle{%s}$" % expr
457 mathmode
= _mathmode()
458 clearmathmode
= attr
.clearclass(_mathmode
)
461 class _phantom(attr
.attr
, textattr
, _localattr
):
464 def apply(self
, expr
):
465 return r
"\phantom{%s}" % expr
468 clearphantom
= attr
.clearclass(_phantom
)
471 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
473 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
479 def __init__(self
, width
, baseline
=top
):
480 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
481 self
.baseline
= baseline
482 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
484 def apply(self
, expr
):
485 if self
.baseline
== self
.top
:
486 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
487 elif self
.baseline
== self
.middle
:
488 return r
"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self
.width
, expr
, expr
)
489 elif self
.baseline
== self
.bottom
:
490 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
492 RuntimeError("invalid baseline argument")
494 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
496 class parbox(parbox_pt
):
498 def __init__(self
, width
, **kwargs
):
499 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
501 parbox
.clear
= parbox_pt
.clear
504 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
506 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
508 def __init__(self
, avalign
):
509 self
.valign
= avalign
510 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
512 def apply(self
, expr
):
513 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
)
515 valign
.top
= valign(0)
516 valign
.middle
= valign(0.5)
517 valign
.bottom
= valign(1)
518 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
521 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
523 class _vshift(attr
.sortbeforeattr
, textattr
):
526 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
528 def apply(self
, expr
):
529 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
531 class vshift(_vshift
):
532 "vertical down shift by a fraction of a character height"
534 def __init__(self
, lowerratio
, heightstr
="0"):
535 _vshift
.__init
__(self
)
536 self
.lowerratio
= lowerratio
537 self
.heightstr
= heightstr
539 def setheightexpr(self
):
540 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
542 class _vshiftmathaxis(_vshift
):
543 "vertical down shift by the height of the math axis"
545 def setheightexpr(self
):
546 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
549 vshift
.bottomzero
= vshift(0)
550 vshift
.middlezero
= vshift(0.5)
551 vshift
.topzero
= vshift(1)
552 vshift
.mathaxis
= _vshiftmathaxis()
553 vshift
.clear
= attr
.clearclass(_vshift
)
556 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
557 None, "tiny", "scriptsize", "footnotesize", "small"]
559 class size(attr
.sortbeforeattr
, textattr
):
562 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
563 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
564 raise RuntimeError("either specify sizeindex or sizename")
565 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
566 if sizeindex
is not None:
567 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
568 self
.size
= sizelist
[sizeindex
]
569 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
570 self
.size
= sizelist
[sizeindex
]
572 raise IndexError("index out of sizelist range")
576 def apply(self
, expr
):
577 return r
"\%s{}%s" % (self
.size
, expr
)
580 size
.scriptsize
= size
.script
= size(-3)
581 size
.footnotesize
= size
.footnote
= size(-2)
582 size
.small
= size(-1)
583 size
.normalsize
= size
.normal
= size(0)
589 size
.clear
= attr
.clearclass(size
)
592 ###############################################################################
594 ###############################################################################
597 class _readpipe(threading
.Thread
):
598 """threaded reader of TeX/LaTeX output
599 - sets an event, when a specific string in the programs output is found
600 - sets an event, when the terminal ends"""
602 def __init__(self
, pipe
, expectqueue
, gotevent
, gotqueue
, quitevent
):
603 """initialize the reader
604 - pipe: file to be read from
605 - expectqueue: keeps the next InputMarker to be wait for
606 - gotevent: the "got InputMarker" event
607 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
608 - quitevent: the "end of terminal" event"""
609 threading
.Thread
.__init
__(self
)
610 self
.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
612 self
.expectqueue
= expectqueue
613 self
.gotevent
= gotevent
614 self
.gotqueue
= gotqueue
615 self
.quitevent
= quitevent
621 read
= self
.pipe
.readline() # read, what comes in
623 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
627 # universal EOL handling (convert everything into unix like EOLs)
628 # XXX is this necessary on pipes?
629 read
= read
.replace("\r", "").replace("\n", "") + "\n"
630 self
.gotqueue
.put(read
) # report, whats read
631 if self
.expect
is not None and read
.find(self
.expect
) != -1:
632 self
.gotevent
.set() # raise the got event, when the output was expected (XXX: within a single line)
633 read
= self
.pipe
.readline() # read again
635 self
.expect
= self
.expectqueue
.get_nowait()
640 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
641 raise RuntimeError("TeX/LaTeX finished unexpectedly")
645 class textbox(box
.rect
, canvas
._canvas
):
646 """basically a box.rect, but it contains a text created by the texrunner
647 - texrunner._text and texrunner.text return such an object
648 - _textbox instances can be inserted into a canvas
649 - the output is contained in a page of the dvifile available thru the texrunner"""
650 # TODO: shouldn't all boxes become canvases? how about inserts then?
652 def __init__(self
, x
, y
, left
, right
, height
, depth
, finishdvi
, attrs
):
654 - finishdvi is a method to be called to get the dvicanvas
655 (e.g. the finishdvi calls the setdvicanvas method)
656 - attrs are fillstyles"""
659 self
.width
= left
+ right
662 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
663 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
664 canvas
._canvas
.__init
__(self
, attrs
)
665 self
.finishdvi
= finishdvi
666 self
.dvicanvas
= None
667 self
.insertdvicanvas
= 0
669 def transform(self
, *trafos
):
670 if self
.insertdvicanvas
:
671 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
672 box
.rect
.transform(self
, *trafos
)
674 self
.texttrafo
= trafo
* self
.texttrafo
676 def setdvicanvas(self
, dvicanvas
):
677 if self
.dvicanvas
is not None:
678 raise RuntimeError("multiple call to setdvicanvas")
679 self
.dvicanvas
= dvicanvas
681 def ensuredvicanvas(self
):
682 if self
.dvicanvas
is None:
684 assert self
.dvicanvas
is not None, "finishdvi is broken"
685 if not self
.insertdvicanvas
:
686 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
687 self
.insertdvicanvas
= 1
689 def marker(self
, marker
):
690 self
.ensuredvicanvas()
691 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
693 def processPS(self
, file, writer
, context
, registry
, bbox
):
694 self
.ensuredvicanvas()
695 abbox
= bboxmodule
.empty()
696 canvas
._canvas
.processPS(self
, file, writer
, context
, registry
, abbox
)
697 bbox
+= box
.rect
.bbox(self
)
699 def processPDF(self
, file, writer
, context
, registry
, bbox
):
700 self
.ensuredvicanvas()
701 abbox
= bboxmodule
.empty()
702 canvas
._canvas
.processPDF(self
, file, writer
, context
, registry
, abbox
)
703 bbox
+= box
.rect
.bbox(self
)
706 def _cleantmp(texrunner
):
707 """get rid of temporary files
708 - function to be registered by atexit
709 - files contained in usefiles are kept"""
710 if texrunner
.texruns
: # cleanup while TeX is still running?
711 texrunner
.expectqueue
.put_nowait(None) # do not expect any output anymore
712 if texrunner
.mode
== "latex": # try to immediately quit from TeX or LaTeX
713 texrunner
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
715 texrunner
.texinput
.write("\n\\end\n")
716 texrunner
.texinput
.close() # close the input queue and
717 if not texrunner
.waitforevent(texrunner
.quitevent
): # wait for finish of the output
718 return # didn't got a quit from TeX -> we can't do much more
719 texrunner
.texruns
= 0
720 texrunner
.texdone
= 1
721 for usefile
in texrunner
.usefiles
:
722 extpos
= usefile
.rfind(".")
724 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
727 for file in glob
.glob("%s.*" % texrunner
.texfilename
):
732 if texrunner
.texdebug
is not None:
734 texrunner
.texdebug
.close()
735 texrunner
.texdebug
= None
744 """TeX/LaTeX interface
745 - runs TeX/LaTeX expressions instantly
746 - checks TeX/LaTeX response
747 - the instance variable texmessage stores the last TeX
749 - the instance variable texmessageparsed stores a parsed
750 version of texmessage; it should be empty after
751 texmessage.check was called, otherwise a TexResultError
753 - the instance variable errordebug controls the verbose
754 level of TexResultError"""
756 defaulttexmessagesstart
= [texmessage
.start
]
757 defaulttexmessagesdocclass
= [texmessage
.load
]
758 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.noaux
]
759 defaulttexmessagesend
= [texmessage
.end
, texmessage
.fontwarning
]
760 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
761 defaulttexmessagesdefaultrun
= [texmessage
.loaddef
, texmessage
.graphicsload
,
762 texmessage
.fontwarning
, texmessage
.boxwarning
]
764 def __init__(self
, mode
="tex",
769 fontmaps
=config
.get("text", "fontmaps", "psfonts.map"),
770 waitfortex
=config
.getint("text", "waitfortex", 60),
771 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
772 texipc
=config
.getboolean("text", "texipc", 0),
778 texmessagesdocclass
=[],
779 texmessagesbegindoc
=[],
781 texmessagesdefaultpreamble
=[],
782 texmessagesdefaultrun
=[]):
784 if mode
!= "tex" and mode
!= "latex":
785 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
788 self
.docclass
= docclass
790 self
.usefiles
= usefiles
791 self
.fontmaps
= fontmaps
792 self
.waitfortex
= waitfortex
793 self
.showwaitfortex
= showwaitfortex
795 if texdebug
is not None:
796 if texdebug
[-4:] == ".tex":
797 self
.texdebug
= open(texdebug
, "w")
799 self
.texdebug
= open("%s.tex" % texdebug
, "w")
802 self
.dvidebug
= dvidebug
803 self
.errordebug
= errordebug
804 self
.pyxgraphics
= pyxgraphics
805 self
.texmessagesstart
= texmessagesstart
806 self
.texmessagesdocclass
= texmessagesdocclass
807 self
.texmessagesbegindoc
= texmessagesbegindoc
808 self
.texmessagesend
= texmessagesend
809 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
810 self
.texmessagesdefaultrun
= texmessagesdefaultrun
814 self
.preamblemode
= 1
818 self
.needdvitextboxes
= [] # when texipc-mode off
820 self
.textboxesincluded
= 0
821 savetempdir
= tempfile
.tempdir
822 tempfile
.tempdir
= os
.curdir
823 self
.texfilename
= os
.path
.basename(tempfile
.mktemp())
824 tempfile
.tempdir
= savetempdir
826 def waitforevent(self
, event
):
827 """waits verbosely with an timeout for an event
828 - observes an event while periodly while printing messages
829 - returns the status of the event (isSet)
830 - does not clear the event"""
831 if self
.showwaitfortex
:
834 while waited
< self
.waitfortex
and not hasevent
:
835 if self
.waitfortex
- waited
> self
.showwaitfortex
:
836 event
.wait(self
.showwaitfortex
)
837 waited
+= self
.showwaitfortex
839 event
.wait(self
.waitfortex
- waited
)
840 waited
+= self
.waitfortex
- waited
841 hasevent
= event
.isSet()
843 if waited
< self
.waitfortex
:
844 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
846 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
849 event
.wait(self
.waitfortex
)
852 def execute(self
, expr
, texmessages
):
853 """executes expr within TeX/LaTeX
854 - if self.texruns is not yet set, TeX/LaTeX is initialized,
855 self.texruns is set and self.preamblemode is set
856 - the method must not be called, when self.texdone is already set
857 - expr should be a string or None
858 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
859 self.texdone becomes set
860 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
861 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
862 - texmessages is a list of texmessage instances"""
864 if self
.texdebug
is not None:
865 self
.texdebug
.write("%% PyX %s texdebug file\n" % version
.version
)
866 self
.texdebug
.write("%% mode: %s\n" % self
.mode
)
867 self
.texdebug
.write("%% date: %s\n" % time
.asctime(time
.localtime(time
.time())))
868 for usefile
in self
.usefiles
:
869 extpos
= usefile
.rfind(".")
871 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
874 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
875 texfile
.write("\\relax%\n")
882 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
884 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
885 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t")
886 atexit
.register(_cleantmp
, self
)
887 self
.expectqueue
= Queue
.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
888 self
.gotevent
= threading
.Event() # keeps the got inputmarker event
889 self
.gotqueue
= Queue
.Queue(0) # allow arbitrary number of entries
890 self
.quitevent
= threading
.Event() # keeps for end of terminal event
891 self
.readoutput
= _readpipe(self
.texoutput
, self
.expectqueue
, self
.gotevent
, self
.gotqueue
, self
.quitevent
)
893 self
.fontmap
= dvifile
.readfontmap(self
.fontmaps
.split())
894 oldpreamblemode
= self
.preamblemode
895 self
.preamblemode
= 1
896 self
.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
897 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
898 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
899 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
900 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
901 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
902 "\\newdimen\\PyXDimenHAlignRT%\n" +
903 _textattrspreamble
+ # insert preambles for textattrs macros
904 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
905 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
906 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
907 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
908 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
909 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
910 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
911 "lt=\\the\\PyXDimenHAlignLT,"
912 "rt=\\the\\PyXDimenHAlignRT,"
913 "ht=\\the\\ht\\PyXBox,"
914 "dp=\\the\\dp\\PyXBox:}%\n"
915 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
916 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
917 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
918 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
919 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
920 self
.defaulttexmessagesstart
+ self
.texmessagesstart
)
921 os
.remove("%s.tex" % self
.texfilename
)
922 if self
.mode
== "tex":
925 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
928 lfsname
= "%s.lfs" % self
.lfs
929 for fulllfsname
in [lfsname
,
930 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
932 lfsfile
= open(fulllfsname
, "r")
933 lfsdef
= lfsfile
.read()
939 lfserror
= "File '%s' is not available or not readable. " % lfsname
942 if lfserror
is not None:
943 allfiles
= (glob
.glob("*.lfs") +
944 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
949 lfsnames
.append(os
.path
.basename(f
)[:-4])
954 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
956 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror
)
957 self
.execute(lfsdef
, [])
958 self
.execute("\\normalsize%\n", [])
959 self
.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
960 elif self
.mode
== "latex":
962 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
964 open(pyxdef
, "r").close()
966 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
967 pyxdef
= os
.path
.abspath(pyxdef
).replace(os
.sep
, "/")
968 self
.execute("\\makeatletter%\n"
969 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
970 "\\def\\ProcessOptions{%\n"
971 "\\def\\Gin@driver{" + pyxdef
+ "}%\n"
972 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
973 "\\saveProcessOptions}%\n"
976 if self
.docopt
is not None:
977 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
978 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
980 self
.execute("\\documentclass{%s}" % self
.docclass
,
981 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
982 self
.preamblemode
= oldpreamblemode
984 if expr
is not None: # TeX/LaTeX should process expr
985 self
.expectqueue
.put_nowait("PyXInputMarker:executeid=%i:" % self
.executeid
)
986 if self
.preamblemode
:
987 self
.expr
= ("%s%%\n" % expr
+
988 "\\PyXInput{%i}%%\n" % self
.executeid
)
991 self
.expr
= ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr
, self
.page
) +
992 "\\PyXInput{%i}%%\n" % self
.executeid
)
993 else: # TeX/LaTeX should be finished
994 self
.expectqueue
.put_nowait("Transcript written on %s.log" % self
.texfilename
)
995 if self
.mode
== "latex":
996 self
.expr
= "\\end{document}%\n"
998 self
.expr
= "\\end%\n"
999 if self
.texdebug
is not None:
1000 self
.texdebug
.write(self
.expr
)
1001 self
.texinput
.write(self
.expr
)
1002 gotevent
= self
.waitforevent(self
.gotevent
)
1003 self
.gotevent
.clear()
1004 if expr
is None and gotevent
: # TeX/LaTeX should have finished
1007 self
.texinput
.close() # close the input queue and
1008 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1010 self
.texmessage
= ""
1012 self
.texmessage
+= self
.gotqueue
.get_nowait()
1015 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1016 self
.texmessageparsed
= self
.texmessage
1018 if expr
is not None:
1019 texmessage
.inputmarker
.check(self
)
1020 if not self
.preamblemode
:
1021 texmessage
.pyxbox
.check(self
)
1022 texmessage
.pyxpageout
.check(self
)
1023 texmessages
= attr
.mergeattrs(texmessages
)
1024 for t
in texmessages
:
1026 keeptexmessageparsed
= self
.texmessageparsed
1027 texmessage
.emptylines
.check(self
)
1028 if len(self
.texmessageparsed
):
1029 self
.texmessageparsed
= keeptexmessageparsed
1030 raise TexResultError("unhandled TeX response (might be an error)", self
)
1032 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
1034 def finishdvi(self
, ignoretail
=0):
1035 """finish TeX/LaTeX and read the dvifile
1036 - this method ensures that all textboxes can access their
1038 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1039 dvifilename
= "%s.dvi" % self
.texfilename
1041 self
.dvifile
= dvifile
.dvifile(dvifilename
, self
.fontmap
, debug
=self
.dvidebug
)
1043 for box
in self
.needdvitextboxes
:
1044 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0]))
1046 if not ignoretail
and self
.dvifile
.readpage(None) is not None:
1047 raise RuntimeError("end of dvifile expected")
1049 self
.needdvitextboxes
= []
1051 def reset(self
, reinit
=0):
1052 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1055 if self
.texdebug
is not None:
1056 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
1061 self
.preamblemode
= 1
1062 for expr
, texmessages
in self
.preambles
:
1063 self
.execute(expr
, texmessages
)
1064 if self
.mode
== "latex":
1065 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1066 self
.preamblemode
= 0
1069 self
.preamblemode
= 1
1071 def set(self
, mode
=_unset
,
1078 showwaitfortex
=_unset
,
1084 texmessagesstart
=_unset
,
1085 texmessagesdocclass
=_unset
,
1086 texmessagesbegindoc
=_unset
,
1087 texmessagesend
=_unset
,
1088 texmessagesdefaultpreamble
=_unset
,
1089 texmessagesdefaultrun
=_unset
):
1090 """provide a set command for TeX/LaTeX settings
1091 - TeX/LaTeX must not yet been started
1092 - especially needed for the defaultrunner, where no access to
1093 the constructor is available"""
1095 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1096 if mode
is not _unset
:
1098 if mode
!= "tex" and mode
!= "latex":
1099 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1101 if lfs
is not _unset
:
1103 if docclass
is not _unset
:
1104 self
.docclass
= docclass
1105 if docopt
is not _unset
:
1106 self
.docopt
= docopt
1107 if usefiles
is not _unset
:
1108 self
.usefiles
= usefiles
1109 if fontmaps
is not _unset
:
1110 self
.fontmaps
= fontmaps
1111 if waitfortex
is not _unset
:
1112 self
.waitfortex
= waitfortex
1113 if showwaitfortex
is not _unset
:
1114 self
.showwaitfortex
= showwaitfortex
1115 if texipc
is not _unset
:
1116 self
.texipc
= texipc
1117 if texdebug
is not _unset
:
1118 if self
.texdebug
is not None:
1119 self
.texdebug
.close()
1120 if texdebug
[-4:] == ".tex":
1121 self
.texdebug
= open(texdebug
, "w")
1123 self
.texdebug
= open("%s.tex" % texdebug
, "w")
1124 if dvidebug
is not _unset
:
1125 self
.dvidebug
= dvidebug
1126 if errordebug
is not _unset
:
1127 self
.errordebug
= errordebug
1128 if pyxgraphics
is not _unset
:
1129 self
.pyxgraphics
= pyxgraphics
1130 if errordebug
is not _unset
:
1131 self
.errordebug
= errordebug
1132 if texmessagesstart
is not _unset
:
1133 self
.texmessagesstart
= texmessagesstart
1134 if texmessagesdocclass
is not _unset
:
1135 self
.texmessagesdocclass
= texmessagesdocclass
1136 if texmessagesbegindoc
is not _unset
:
1137 self
.texmessagesbegindoc
= texmessagesbegindoc
1138 if texmessagesend
is not _unset
:
1139 self
.texmessagesend
= texmessagesend
1140 if texmessagesdefaultpreamble
is not _unset
:
1141 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
1142 if texmessagesdefaultrun
is not _unset
:
1143 self
.texmessagesdefaultrun
= texmessagesdefaultrun
1145 def preamble(self
, expr
, texmessages
=[]):
1146 r
"""put something into the TeX/LaTeX preamble
1147 - in LaTeX, this is done before the \begin{document}
1148 (you might use \AtBeginDocument, when you're in need for)
1149 - it is not allowed to call preamble after calling the
1150 text method for the first time (for LaTeX this is needed
1151 due to \begin{document}; in TeX it is forced for compatibility
1152 (you should be able to switch from TeX to LaTeX, if you want,
1153 without breaking something)
1154 - preamble expressions must not create any dvi output
1155 - args might contain texmessage instances"""
1156 if self
.texdone
or not self
.preamblemode
:
1157 raise RuntimeError("preamble calls disabled due to previous text calls")
1158 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmessagesdefaultpreamble
+ texmessages
1159 self
.execute(expr
, texmessages
)
1160 self
.preambles
.append((expr
, texmessages
))
1162 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:")
1164 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[]):
1165 """create text by passing expr to TeX/LaTeX
1166 - returns a textbox containing the result from running expr thru TeX/LaTeX
1167 - the box center is set to x, y
1168 - *args may contain attr parameters, namely:
1169 - textattr instances
1170 - texmessage instances
1171 - trafo._trafo instances
1172 - style.fillstyle instances"""
1174 raise ValueError("None expression is invalid")
1176 self
.reset(reinit
=1)
1178 if self
.preamblemode
:
1179 if self
.mode
== "latex":
1180 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1181 self
.preamblemode
= 0
1183 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1184 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1185 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1186 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1187 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1188 # reverse loop over the merged textattrs (last is applied first)
1189 lentextattrs
= len(textattrs
)
1190 for i
in range(lentextattrs
):
1191 expr
= textattrs
[lentextattrs
-1-i
].apply(expr
)
1193 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1194 except TexResultError
:
1195 self
.finishdvi(ignoretail
=1)
1199 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1200 match
= self
.PyXBoxPattern
.search(self
.texmessage
)
1201 if not match
or int(match
.group("page")) != self
.page
:
1202 raise TexResultError("box extents not found", self
)
1203 left
, right
, height
, depth
= [float(xxx
)*72/72.27*unit
.x_pt
for xxx
in match
.group("lt", "rt", "ht", "dp")]
1204 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.finishdvi
, fillstyles
)
1208 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0]))
1210 self
.needdvitextboxes
.append(box
)
1213 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1214 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1216 PyXVariableBoxPattern
= re
.compile(r
"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1218 def textboxes(self
, text
, pageshapes
):
1219 # this is some experimental code to put text into several boxes
1220 # while the bounding shape changes from box to box (rectangles only)
1221 # first we load sev.tex
1222 if not self
.textboxesincluded
:
1223 self
.execute(r
"\input textboxes.tex", [texmessage
.load
])
1224 self
.textboxesincluded
= 1
1225 # define page shapes
1226 pageshapes_str
= "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit
.topt(pageshapes
[0][0]), 72.27/72*unit
.topt(pageshapes
[0][1]))
1227 pageshapes_str
+= "\\lohsizes={%\n"
1228 for hsize
, vsize
in pageshapes
[1:]:
1229 pageshapes_str
+= "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(hsize
))
1230 pageshapes_str
+= "{\\relax}%\n}%\n"
1231 pageshapes_str
+= "\\lovsizes={%\n"
1232 for hsize
, vsize
in pageshapes
[1:]:
1233 pageshapes_str
+= "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(vsize
))
1234 pageshapes_str
+= "{\\relax}%\n}%\n"
1240 self
.execute(pageshapes_str
, [])
1241 parnos_str
= "}{".join(parnos
)
1243 parnos_str
= "{%s}" % parnos_str
1244 parnos_str
= "\\parnos={%s{\\relax}}%%\n" % parnos_str
1245 self
.execute(parnos_str
, [])
1246 parshapes_str
= "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes
)
1247 self
.execute(parshapes_str
, [])
1248 self
.execute("\\global\\count0=1%%\n"
1249 "\\global\\parno=0%%\n"
1250 "\\global\\myprevgraf=0%%\n"
1251 "\\global\\showprevgraf=0%%\n"
1252 "\\global\\outputtype=0%%\n"
1253 "\\global\\leastcost=10000000%%\n"
1255 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1257 if self
.dvifile
is None:
1258 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1260 raise RuntimeError("textboxes currently needs texipc")
1263 lastparshapes
= parshapes
1266 lastpar
= prevgraf
= -1
1267 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
1270 page
= int(m
.group("page"))
1271 assert page
== pages
1272 par
= int(m
.group("par"))
1273 prevgraf
= int(m
.group("prevgraf"))
1274 if page
<= len(pageshapes
):
1275 width
= 72.27/72*unit
.topt(pageshapes
[page
-1][0])
1277 width
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1278 if page
< len(pageshapes
):
1279 nextwidth
= 72.27/72*unit
.topt(pageshapes
[page
][0])
1281 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1284 # a new paragraph is to be broken
1285 parnos
.append(str(par
))
1286 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1288 parshape
= " 0pt " + parshape
1289 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1290 elif prevgraf
== lastprevgraf
:
1293 # we have to append the breaking of the previous paragraph
1294 oldparshape
= " ".join(parshapes
[-1].split(' ')[2:2+2*lastprevgraf
])
1295 oldparshape
= oldparshape
.split('}')[0]
1297 oldparshape
= " " + oldparshape
1298 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1300 parshape
= " 0pt " + parshape
1303 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1305 lastprevgraf
= prevgraf
1307 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
1309 for i
in range(pages
):
1310 result
.append(self
.dvifile
.readpage([i
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1311 if parnos
== lastparnos
and parshapes
== lastparshapes
:
1315 raise TexResultError("Too many loops in textboxes ", texrunner
)
1318 # the module provides an default texrunner and methods for direct access
1319 defaulttexrunner
= texrunner()
1320 reset
= defaulttexrunner
.reset
1321 set = defaulttexrunner
.set
1322 preamble
= defaulttexrunner
.preamble
1323 text
= defaulttexrunner
.text
1324 text_pt
= defaulttexrunner
.text_pt
1326 def escapestring(s
, replace
={" ": "~",
1338 "\\": "{$\setminus$}",
1340 "escape all ascii characters such that they are printable by TeX/LaTeX"
1343 if not 32 <= ord(s
[i
]) < 127:
1344 raise ValueError("escapestring function handles ascii strings only")
1351 s
= s
[:i
] + r
+ s
[i
+1:]