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
, traceback
, re
, tempfile
, atexit
, time
, warnings
26 import config
, siteconfig
, unit
, box
, canvas
, trafo
, version
, attr
, style
, dvifile
28 ###############################################################################
30 # - please don't get confused:
31 # - there is a texmessage (and a texmessageparsed) attribute within the
32 # texrunner; it contains TeX/LaTeX response from the last command execution
33 # - instances of classes derived from the class texmessage are used to
34 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
35 # attribute of a texrunner instance
36 # - the multiple usage of the name texmessage might be removed in the future
37 # - texmessage instances should implement _Itexmessage
38 ###############################################################################
40 class TexResultError(RuntimeError):
41 """specialized texrunner exception class
42 - it is raised by texmessage instances, when a texmessage indicates an error
43 - it is raised by the texrunner itself, whenever there is a texmessage left
44 after all parsing of this message (by texmessage instances)"""
46 def __init__(self
, description
, texrunner
):
47 self
.description
= description
48 self
.texrunner
= texrunner
51 """prints a detailed report about the problem
52 - the verbose level is controlled by texrunner.errordebug"""
53 if self
.texrunner
.errordebug
>= 2:
54 return ("%s\n" % self
.description
+
55 "The expression passed to TeX was:\n"
56 " %s\n" % self
.texrunner
.expr
.replace("\n", "\n ").rstrip() +
57 "The return message from TeX was:\n"
58 " %s\n" % self
.texrunner
.texmessage
.replace("\n", "\n ").rstrip() +
59 "After parsing this message, the following was left:\n"
60 " %s" % self
.texrunner
.texmessageparsed
.replace("\n", "\n ").rstrip())
61 elif self
.texrunner
.errordebug
== 1:
62 firstlines
= self
.texrunner
.texmessageparsed
.split("\n")
63 if len(firstlines
) > 5:
64 firstlines
= firstlines
[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
65 return ("%s\n" % self
.description
+
66 "The expression passed to TeX was:\n"
67 " %s\n" % self
.texrunner
.expr
.replace("\n", "\n ").rstrip() +
68 "After parsing the return message from TeX, the following was left:\n" +
69 reduce(lambda x
, y
: "%s %s\n" % (x
,y
), firstlines
, "").rstrip())
71 return self
.description
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("*\n", "")
236 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.replace("\n", "")
239 class _texmessageload(texmessage
):
240 """validates inclusion of arbitrary files
241 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
242 <fielname> is a readable file and other stuff can be anything
243 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
244 - this is not always wanted, but we just assume that file inclusion is fine"""
246 __implements__
= _Itexmessage
248 pattern
= re
.compile(r
"\((?P<filename>[^()\s\n]+)(?P<additional>[^()]*)\)")
250 def baselevels(self
, s
, maxlevel
=1, brackets
="()"):
251 """strip parts of a string above a given bracket level
252 - return a modified (some parts might be removed) version of the string s
253 where all parts inside brackets with level higher than maxlevel are
255 - if brackets do not match (number of left and right brackets is wrong
256 or at some points there were more right brackets than left brackets)
257 just return the unmodified string"""
264 if level
> highestlevel
:
266 if level
<= maxlevel
:
270 if level
== 0 and highestlevel
> 0:
273 def check(self
, texrunner
):
274 lowestbracketlevel
= self
.baselevels(texrunner
.texmessageparsed
)
275 if lowestbracketlevel
is not None:
276 m
= self
.pattern
.search(lowestbracketlevel
)
278 filename
= m
.group("filename").replace("\n", "")
280 additional
= m
.group("additional")
283 if (os
.access(filename
, os
.R_OK
) or
284 len(additional
) and additional
[0] == "\n" and os
.access(filename
+additional
.split()[0], os
.R_OK
)):
285 lowestbracketlevel
= lowestbracketlevel
[:m
.start()] + lowestbracketlevel
[m
.end():]
288 m
= self
.pattern
.search(lowestbracketlevel
)
290 texrunner
.texmessageparsed
= lowestbracketlevel
293 class _texmessageloadfd(_texmessageload
):
294 """validates the inclusion of font description files (fd-files)
295 - works like _texmessageload
296 - filename must end with .fd and no further text is allowed"""
298 pattern
= re
.compile(r
"\((?P<filename>[^)]+.fd)\)")
300 def baselevels(self
, s
, **kwargs
):
304 class _texmessagegraphicsload(_texmessageload
):
305 """validates the inclusion of files as the graphics packages writes it
306 - works like _texmessageload, but using "<" and ">" as delimiters
307 - filename must end with .eps and no further text is allowed"""
309 pattern
= re
.compile(r
"<(?P<filename>[^>]+.eps)>")
311 def baselevels(self
, s
, **kwargs
):
315 class _texmessageignore(_texmessageload
):
316 """validates any TeX/LaTeX response
317 - this might be used, when the expression is ok, but no suitable texmessage
319 - PLEASE: - consider writing suitable tex message parsers
320 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
322 __implements__
= _Itexmessage
324 def check(self
, texrunner
):
325 texrunner
.texmessageparsed
= ""
328 texmessage
.start
= _texmessagestart()
329 texmessage
.noaux
= _texmessagenoaux()
330 texmessage
.end
= _texmessageend()
331 texmessage
.load
= _texmessageload()
332 texmessage
.loadfd
= _texmessageloadfd()
333 texmessage
.graphicsload
= _texmessagegraphicsload()
334 texmessage
.ignore
= _texmessageignore()
337 texmessage
.inputmarker
= _texmessageinputmarker()
338 texmessage
.pyxbox
= _texmessagepyxbox()
339 texmessage
.pyxpageout
= _texmessagepyxpageout()
340 texmessage
.emptylines
= _texmessageemptylines()
343 class _texmessageallwarning(texmessage
):
344 """validates a given pattern 'pattern' as a warning 'warning'"""
346 def check(self
, texrunner
):
347 if texrunner
.texmessageparsed
:
348 warnings
.warn("ignoring all warnings:\n%s" % texrunner
.texmessageparsed
)
349 texrunner
.texmessageparsed
= ""
351 texmessage
.allwarning
= _texmessageallwarning()
354 class texmessagepattern(texmessage
):
355 """validates a given pattern and issue a warning (when set)"""
357 def __init__(self
, pattern
, warning
=None):
358 self
.pattern
= pattern
359 self
.warning
= warning
361 def check(self
, texrunner
):
362 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
364 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
366 warnings
.warn("%s:\n%s" % (self
.warning
, m
.string
[m
.start(): m
.end()].rstrip()))
367 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
369 texmessage
.fontwarning
= texmessagepattern(re
.compile(r
"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re
.MULTILINE
), "ignoring font warning")
370 texmessage
.boxwarning
= texmessagepattern(re
.compile(r
"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re
.MULTILINE
), "ignoring overfull/underfull box warning")
374 ###############################################################################
376 ###############################################################################
378 _textattrspreamble
= ""
381 "a textattr defines a apply method, which modifies a (La)TeX expression"
383 class _localattr
: pass
385 _textattrspreamble
+= r
"""\gdef\PyXFlushHAlign{0}%
386 \newdimen\PyXraggedskipplus%
387 \def\PyXragged{\PyXraggedskipplus=4em%
388 \leftskip=0pt plus \PyXFlushHAlign\PyXraggedskipplus%
389 \rightskip=0pt plus \PyXraggedskipplus%
390 \advance\rightskip0pt plus -\PyXFlushHAlign\PyXraggedskipplus%
398 \exhyphenpenalty=9999}%
401 class boxhalign(attr
.exclusiveattr
, textattr
, _localattr
):
403 def __init__(self
, aboxhalign
):
404 self
.boxhalign
= aboxhalign
405 attr
.exclusiveattr
.__init
__(self
, boxhalign
)
407 def apply(self
, expr
):
408 return r
"\gdef\PyXBoxHAlign{%.5f}%s" % (self
.boxhalign
, expr
)
410 boxhalign
.left
= boxhalign(0)
411 boxhalign
.center
= boxhalign(0.5)
412 boxhalign
.right
= boxhalign(1)
413 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
416 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
418 def __init__(self
, aflushhalign
):
419 self
.flushhalign
= aflushhalign
420 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
422 def apply(self
, expr
):
423 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
425 flushhalign
.left
= flushhalign(0)
426 flushhalign
.center
= flushhalign(0.5)
427 flushhalign
.right
= flushhalign(1)
428 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
431 class halign(attr
.exclusiveattr
, textattr
, boxhalign
, flushhalign
, _localattr
):
433 def __init__(self
, aboxhalign
, aflushhalign
):
434 self
.boxhalign
= aboxhalign
435 self
.flushhalign
= aflushhalign
436 attr
.exclusiveattr
.__init
__(self
, halign
)
438 def apply(self
, expr
):
439 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
441 halign
.left
= halign(0, 0)
442 halign
.center
= halign(0.5, 0.5)
443 halign
.right
= halign(1, 1)
444 halign
.clear
= attr
.clearclass(halign
)
445 halign
.boxleft
= boxhalign
.left
446 halign
.boxcenter
= boxhalign
.center
447 halign
.boxright
= boxhalign
.right
448 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
449 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
450 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
453 class _mathmode(attr
.attr
, textattr
, _localattr
):
456 def apply(self
, expr
):
457 return r
"$\displaystyle{%s}$" % expr
459 mathmode
= _mathmode()
460 clearmathmode
= attr
.clearclass(_mathmode
)
463 class _phantom(attr
.attr
, textattr
, _localattr
):
466 def apply(self
, expr
):
467 return r
"\phantom{%s}" % expr
470 clearphantom
= attr
.clearclass(_phantom
)
473 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
474 None, "tiny", "scriptsize", "footnotesize", "small"]
476 class size(attr
.sortbeforeattr
, textattr
, _localattr
):
479 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
480 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
481 raise RuntimeError("either specify sizeindex or sizename")
482 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
])
483 if sizeindex
is not None:
484 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
485 self
.size
= sizelist
[sizeindex
]
486 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
487 self
.size
= sizelist
[sizeindex
]
489 raise IndexError("index out of sizelist range")
493 def apply(self
, expr
):
494 return r
"\%s{%s}" % (self
.size
, expr
)
497 size
.scriptsize
= size
.script
= size(-3)
498 size
.footnotesize
= size
.footnote
= size(-2)
499 size
.small
= size(-1)
500 size
.normalsize
= size
.normal
= size(0)
506 size
.clear
= attr
.clearclass(size
)
509 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\PyXDimenVBox%\n"
511 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
517 def __init__(self
, width
, baseline
=top
):
518 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
519 self
.baseline
= baseline
520 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
522 def apply(self
, expr
):
523 if self
.baseline
== self
.top
:
524 return r
"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self
.width
, expr
)
525 elif self
.baseline
== self
.middle
:
526 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
)
527 elif self
.baseline
== self
.bottom
:
528 return r
"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self
.width
, expr
)
530 RuntimeError("invalid baseline argument")
532 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
534 class parbox(parbox_pt
):
536 def __init__(self
, width
, **kwargs
):
537 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
539 parbox
.clear
= parbox_pt
.clear
542 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\PyXDimenVAlign%\n"
544 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
546 def __init__(self
, avalign
):
547 self
.valign
= avalign
548 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
550 def apply(self
, expr
):
551 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
)
553 valign
.top
= valign(0)
554 valign
.middle
= valign(0.5)
555 valign
.bottom
= valign(1)
556 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
559 class _vshift(attr
.sortbeforeattr
, textattr
):
562 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
564 class vshift(_vshift
):
565 "vertical down shift by a fraction of a character height"
567 def __init__(self
, lowerratio
, heightstr
="0"):
568 _vshift
.__init
__(self
)
569 self
.lowerratio
= lowerratio
570 self
.heightstr
= heightstr
572 def apply(self
, expr
):
573 return r
"\setbox0\hbox{{%s}}\lower%.5f\ht0\hbox{{%s}}" % (self
.heightstr
, self
.lowerratio
, expr
)
575 class _vshiftmathaxis(_vshift
):
576 "vertical down shift by the height of the math axis"
578 def apply(self
, expr
):
579 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\lower\ht0\hbox{{%s}}" % expr
582 vshift
.bottomzero
= vshift(0)
583 vshift
.middlezero
= vshift(0.5)
584 vshift
.topzero
= vshift(1)
585 vshift
.mathaxis
= _vshiftmathaxis()
586 vshift
.clear
= attr
.clearclass(_vshift
)
589 ###############################################################################
591 ###############################################################################
594 class _readpipe(threading
.Thread
):
595 """threaded reader of TeX/LaTeX output
596 - sets an event, when a specific string in the programs output is found
597 - sets an event, when the terminal ends"""
599 def __init__(self
, pipe
, expectqueue
, gotevent
, gotqueue
, quitevent
):
600 """initialize the reader
601 - pipe: file to be read from
602 - expectqueue: keeps the next InputMarker to be wait for
603 - gotevent: the "got InputMarker" event
604 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
605 - quitevent: the "end of terminal" event"""
606 threading
.Thread
.__init
__(self
)
607 self
.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
609 self
.expectqueue
= expectqueue
610 self
.gotevent
= gotevent
611 self
.gotqueue
= gotqueue
612 self
.quitevent
= quitevent
618 read
= self
.pipe
.readline() # read, what comes in
620 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
624 # universal EOL handling (convert everything into unix like EOLs)
625 # XXX is this necessary on pipes?
626 read
= read
.replace("\r", "").replace("\n", "") + "\n"
627 self
.gotqueue
.put(read
) # report, whats read
628 if self
.expect
is not None and read
.find(self
.expect
) != -1:
629 self
.gotevent
.set() # raise the got event, when the output was expected (XXX: within a single line)
630 read
= self
.pipe
.readline() # read again
632 self
.expect
= self
.expectqueue
.get_nowait()
637 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
638 raise RuntimeError("TeX/LaTeX finished unexpectedly")
642 class textbox(box
.rect
, canvas
._canvas
):
643 """basically a box.rect, but it contains a text created by the texrunner
644 - texrunner._text and texrunner.text return such an object
645 - _textbox instances can be inserted into a canvas
646 - the output is contained in a page of the dvifile available thru the texrunner"""
647 # TODO: shouldn't all boxes become canvases? how about inserts then?
649 def __init__(self
, x
, y
, left
, right
, height
, depth
, finishdvi
, attrs
):
651 - finishdvi is a method to be called to get the dvicanvas
652 (e.g. the finishdvi calls the setdvicanvas method)
653 - attrs are fillstyles"""
656 self
.width
= left
+ right
659 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
660 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
661 canvas
._canvas
.__init
__(self
)
662 self
.finishdvi
= finishdvi
663 self
.dvicanvas
= None
665 self
.insertdvicanvas
= 0
667 def transform(self
, *trafos
):
668 if self
.insertdvicanvas
:
669 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
670 box
.rect
.transform(self
, *trafos
)
672 self
.texttrafo
= trafo
* self
.texttrafo
674 def setdvicanvas(self
, dvicanvas
):
675 if self
.dvicanvas
is not None:
676 raise RuntimeError("multiple call to setdvicanvas")
677 self
.dvicanvas
= dvicanvas
679 def ensuredvicanvas(self
):
680 if self
.dvicanvas
is None:
682 assert self
.dvicanvas
is not None, "finishdvi is broken"
683 if not self
.insertdvicanvas
:
684 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
685 self
.insertdvicanvas
= 1
687 def marker(self
, marker
):
688 self
.ensuredvicanvas()
689 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
691 def registerPS(self
, registry
):
692 self
.ensuredvicanvas()
693 canvas
._canvas
.registerPS(self
, registry
)
695 def registerPDF(self
, registry
):
696 self
.ensuredvicanvas()
697 canvas
._canvas
.registerPDF(self
, registry
)
699 def outputPS(self
, file, writer
, context
):
700 self
.ensuredvicanvas()
701 canvas
._canvas
.outputPS(self
, file, writer
, context
)
703 def outputPDF(self
, file, writer
, context
):
704 self
.ensuredvicanvas()
705 canvas
._canvas
.outputPDF(self
, file, writer
, context
)
708 def _cleantmp(texrunner
):
709 """get rid of temporary files
710 - function to be registered by atexit
711 - files contained in usefiles are kept"""
712 if texrunner
.texruns
: # cleanup while TeX is still running?
713 texrunner
.expectqueue
.put_nowait(None) # do not expect any output anymore
714 if texrunner
.mode
== "latex": # try to immediately quit from TeX or LaTeX
715 texrunner
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
717 texrunner
.texinput
.write("\n\\end\n")
718 texrunner
.texinput
.close() # close the input queue and
719 if not texrunner
.waitforevent(texrunner
.quitevent
): # wait for finish of the output
720 return # didn't got a quit from TeX -> we can't do much more
721 texrunner
.texruns
= 0
722 texrunner
.texdone
= 1
723 for usefile
in texrunner
.usefiles
:
724 extpos
= usefile
.rfind(".")
726 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
729 for file in glob
.glob("%s.*" % texrunner
.texfilename
):
734 if texrunner
.texdebug
is not None:
736 texrunner
.texdebug
.close()
737 texrunner
.texdebug
= None
746 """TeX/LaTeX interface
747 - runs TeX/LaTeX expressions instantly
748 - checks TeX/LaTeX response
749 - the instance variable texmessage stores the last TeX
751 - the instance variable texmessageparsed stores a parsed
752 version of texmessage; it should be empty after
753 texmessage.check was called, otherwise a TexResultError
755 - the instance variable errordebug controls the verbose
756 level of TexResultError"""
758 defaulttexmessagesstart
= [texmessage
.start
]
759 defaulttexmessagesdocclass
= [texmessage
.load
]
760 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.noaux
]
761 defaulttexmessagesend
= [texmessage
.end
, texmessage
.fontwarning
]
762 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
763 defaulttexmessagesdefaultrun
= [texmessage
.loadfd
, texmessage
.graphicsload
,
764 texmessage
.fontwarning
, texmessage
.boxwarning
]
766 def __init__(self
, mode
="tex",
771 fontmaps
=config
.get("text", "fontmaps", "psfonts.map"),
772 waitfortex
=config
.getint("text", "waitfortex", 60),
773 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
774 texipc
=config
.getboolean("text", "texipc", 0),
780 texmessagesdocclass
=[],
781 texmessagesbegindoc
=[],
783 texmessagesdefaultpreamble
=[],
784 texmessagesdefaultrun
=[]):
786 if mode
!= "tex" and mode
!= "latex":
787 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
790 self
.docclass
= docclass
792 self
.usefiles
= usefiles
793 self
.fontmaps
= fontmaps
794 self
.waitfortex
= waitfortex
795 self
.showwaitfortex
= showwaitfortex
797 if texdebug
is not None:
798 if texdebug
[-4:] == ".tex":
799 self
.texdebug
= open(texdebug
, "w")
801 self
.texdebug
= open("%s.tex" % texdebug
, "w")
804 self
.dvidebug
= dvidebug
805 self
.errordebug
= errordebug
806 self
.pyxgraphics
= pyxgraphics
807 self
.texmessagesstart
= texmessagesstart
808 self
.texmessagesdocclass
= texmessagesdocclass
809 self
.texmessagesbegindoc
= texmessagesbegindoc
810 self
.texmessagesend
= texmessagesend
811 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
812 self
.texmessagesdefaultrun
= texmessagesdefaultrun
816 self
.preamblemode
= 1
820 self
.needdvitextboxes
= [] # when texipc-mode off
822 self
.textboxesincluded
= 0
823 savetempdir
= tempfile
.tempdir
824 tempfile
.tempdir
= os
.curdir
825 self
.texfilename
= os
.path
.basename(tempfile
.mktemp())
826 tempfile
.tempdir
= savetempdir
828 def waitforevent(self
, event
):
829 """waits verbosely with an timeout for an event
830 - observes an event while periodly while printing messages
831 - returns the status of the event (isSet)
832 - does not clear the event"""
833 if self
.showwaitfortex
:
836 while waited
< self
.waitfortex
and not hasevent
:
837 if self
.waitfortex
- waited
> self
.showwaitfortex
:
838 event
.wait(self
.showwaitfortex
)
839 waited
+= self
.showwaitfortex
841 event
.wait(self
.waitfortex
- waited
)
842 waited
+= self
.waitfortex
- waited
843 hasevent
= event
.isSet()
845 if waited
< self
.waitfortex
:
846 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
848 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
851 event
.wait(self
.waitfortex
)
854 def execute(self
, expr
, texmessages
):
855 """executes expr within TeX/LaTeX
856 - if self.texruns is not yet set, TeX/LaTeX is initialized,
857 self.texruns is set and self.preamblemode is set
858 - the method must not be called, when self.texdone is already set
859 - expr should be a string or None
860 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
861 self.texdone becomes set
862 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
863 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
864 - texmessages is a list of texmessage instances"""
866 if self
.texdebug
is not None:
867 self
.texdebug
.write("%% PyX %s texdebug file\n" % version
.version
)
868 self
.texdebug
.write("%% mode: %s\n" % self
.mode
)
869 self
.texdebug
.write("%% date: %s\n" % time
.asctime(time
.localtime(time
.time())))
870 for usefile
in self
.usefiles
:
871 extpos
= usefile
.rfind(".")
873 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
876 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
877 texfile
.write("\\relax%\n")
884 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
886 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
887 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t")
888 atexit
.register(_cleantmp
, self
)
889 self
.expectqueue
= Queue
.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
890 self
.gotevent
= threading
.Event() # keeps the got inputmarker event
891 self
.gotqueue
= Queue
.Queue(0) # allow arbitrary number of entries
892 self
.quitevent
= threading
.Event() # keeps for end of terminal event
893 self
.readoutput
= _readpipe(self
.texoutput
, self
.expectqueue
, self
.gotevent
, self
.gotqueue
, self
.quitevent
)
895 self
.fontmap
= dvifile
.readfontmap(self
.fontmaps
.split())
896 oldpreamblemode
= self
.preamblemode
897 self
.preamblemode
= 1
898 self
.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
899 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
900 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
901 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
902 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
903 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
904 "\\newdimen\\PyXDimenHAlignRT%\n" +
905 _textattrspreamble
+ # insert preambles for textattrs macros
906 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
907 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
908 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
909 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
910 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
911 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
912 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
913 "lt=\\the\\PyXDimenHAlignLT,"
914 "rt=\\the\\PyXDimenHAlignRT,"
915 "ht=\\the\\ht\\PyXBox,"
916 "dp=\\the\\dp\\PyXBox:}%\n"
917 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
918 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
919 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
920 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
921 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
922 self
.defaulttexmessagesstart
+ self
.texmessagesstart
)
923 os
.remove("%s.tex" % self
.texfilename
)
924 if self
.mode
== "tex":
927 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
930 lfsname
= "%s.lfs" % self
.lfs
931 for fulllfsname
in [lfsname
,
932 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
934 lfsfile
= open(fulllfsname
, "r")
935 lfsdef
= lfsfile
.read()
941 lfserror
= "File '%s' is not available or not readable. " % lfsname
944 if lfserror
is not None:
945 allfiles
= (glob
.glob("*.lfs") +
946 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
951 lfsnames
.append(os
.path
.basename(f
)[:-4])
956 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
958 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror
)
959 self
.execute(lfsdef
, [])
960 self
.execute("\\normalsize%\n", [])
961 self
.execute("\\newdimen\\linewidth%\n", [])
962 elif self
.mode
== "latex":
964 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
966 open(pyxdef
, "r").close()
968 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
969 pyxdef
= os
.path
.abspath(pyxdef
).replace(os
.sep
, "/")
970 self
.execute("\\makeatletter%\n"
971 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
972 "\\def\\ProcessOptions{%\n"
973 "\\def\\Gin@driver{" + pyxdef
+ "}%\n"
974 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
975 "\\saveProcessOptions}%\n"
978 if self
.docopt
is not None:
979 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
980 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
982 self
.execute("\\documentclass{%s}" % self
.docclass
,
983 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
984 self
.preamblemode
= oldpreamblemode
986 if expr
is not None: # TeX/LaTeX should process expr
987 self
.expectqueue
.put_nowait("PyXInputMarker:executeid=%i:" % self
.executeid
)
988 if self
.preamblemode
:
989 self
.expr
= ("%s%%\n" % expr
+
990 "\\PyXInput{%i}%%\n" % self
.executeid
)
993 self
.expr
= ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr
, self
.page
) +
994 "\\PyXInput{%i}%%\n" % self
.executeid
)
995 else: # TeX/LaTeX should be finished
996 self
.expectqueue
.put_nowait("Transcript written on %s.log" % self
.texfilename
)
997 if self
.mode
== "latex":
998 self
.expr
= "\\end{document}%\n"
1000 self
.expr
= "\\end%\n"
1001 if self
.texdebug
is not None:
1002 self
.texdebug
.write(self
.expr
)
1003 self
.texinput
.write(self
.expr
)
1004 gotevent
= self
.waitforevent(self
.gotevent
)
1005 self
.gotevent
.clear()
1006 if expr
is None and gotevent
: # TeX/LaTeX should have finished
1009 self
.texinput
.close() # close the input queue and
1010 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1012 self
.texmessage
= ""
1014 self
.texmessage
+= self
.gotqueue
.get_nowait()
1017 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1018 self
.texmessageparsed
= self
.texmessage
1020 if expr
is not None:
1021 texmessage
.inputmarker
.check(self
)
1022 if not self
.preamblemode
:
1023 texmessage
.pyxbox
.check(self
)
1024 texmessage
.pyxpageout
.check(self
)
1025 texmessages
= attr
.mergeattrs(texmessages
)
1026 for t
in texmessages
:
1028 keeptexmessageparsed
= self
.texmessageparsed
1029 texmessage
.emptylines
.check(self
)
1030 if len(self
.texmessageparsed
):
1031 self
.texmessageparsed
= keeptexmessageparsed
1032 raise TexResultError("unhandled TeX response (might be an error)", self
)
1034 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
1036 def finishdvi(self
):
1037 """finish TeX/LaTeX and read the dvifile
1038 - this method ensures that all textboxes can access their
1040 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1041 dvifilename
= "%s.dvi" % self
.texfilename
1043 self
.dvifile
= dvifile
.dvifile(dvifilename
, self
.fontmap
, debug
=self
.dvidebug
)
1045 for box
in self
.needdvitextboxes
:
1046 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0]))
1048 if self
.dvifile
.readpage(None) is not None:
1049 raise RuntimeError("end of dvifile expected")
1051 self
.needdvitextboxes
= []
1053 def reset(self
, reinit
=0):
1054 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1057 if self
.texdebug
is not None:
1058 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
1063 self
.preamblemode
= 1
1064 for expr
, texmessages
in self
.preambles
:
1065 self
.execute(expr
, texmessages
)
1066 if self
.mode
== "latex":
1067 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1068 self
.preamblemode
= 0
1071 self
.preamblemode
= 1
1073 def set(self
, mode
=_unset
,
1080 showwaitfortex
=_unset
,
1086 texmessagesstart
=_unset
,
1087 texmessagesdocclass
=_unset
,
1088 texmessagesbegindoc
=_unset
,
1089 texmessagesend
=_unset
,
1090 texmessagesdefaultpreamble
=_unset
,
1091 texmessagesdefaultrun
=_unset
):
1092 """provide a set command for TeX/LaTeX settings
1093 - TeX/LaTeX must not yet been started
1094 - especially needed for the defaultrunner, where no access to
1095 the constructor is available"""
1097 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1098 if mode
is not _unset
:
1100 if mode
!= "tex" and mode
!= "latex":
1101 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1103 if lfs
is not _unset
:
1105 if docclass
is not _unset
:
1106 self
.docclass
= docclass
1107 if docopt
is not _unset
:
1108 self
.docopt
= docopt
1109 if usefiles
is not _unset
:
1110 self
.usefiles
= usefiles
1111 if fontmaps
is not _unset
:
1112 self
.fontmaps
= fontmaps
1113 if waitfortex
is not _unset
:
1114 self
.waitfortex
= waitfortex
1115 if showwaitfortex
is not _unset
:
1116 self
.showwaitfortex
= showwaitfortex
1117 if texipc
is not _unset
:
1118 self
.texipc
= texipc
1119 if texdebug
is not _unset
:
1120 if self
.texdebug
is not None:
1121 self
.texdebug
.close()
1122 if texdebug
[-4:] == ".tex":
1123 self
.texdebug
= open(texdebug
, "w")
1125 self
.texdebug
= open("%s.tex" % texdebug
, "w")
1126 if dvidebug
is not _unset
:
1127 self
.dvidebug
= dvidebug
1128 if errordebug
is not _unset
:
1129 self
.errordebug
= errordebug
1130 if pyxgraphics
is not _unset
:
1131 self
.pyxgraphics
= pyxgraphics
1132 if errordebug
is not _unset
:
1133 self
.errordebug
= errordebug
1134 if texmessagesstart
is not _unset
:
1135 self
.texmessagesstart
= texmessagesstart
1136 if texmessagesdocclass
is not _unset
:
1137 self
.texmessagesdocclass
= texmessagesdocclass
1138 if texmessagesbegindoc
is not _unset
:
1139 self
.texmessagesbegindoc
= texmessagesbegindoc
1140 if texmessagesend
is not _unset
:
1141 self
.texmessagesend
= texmessagesend
1142 if texmessagesdefaultpreamble
is not _unset
:
1143 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
1144 if texmessagesdefaultrun
is not _unset
:
1145 self
.texmessagesdefaultrun
= texmessagesdefaultrun
1147 def preamble(self
, expr
, texmessages
=[]):
1148 r
"""put something into the TeX/LaTeX preamble
1149 - in LaTeX, this is done before the \begin{document}
1150 (you might use \AtBeginDocument, when you're in need for)
1151 - it is not allowed to call preamble after calling the
1152 text method for the first time (for LaTeX this is needed
1153 due to \begin{document}; in TeX it is forced for compatibility
1154 (you should be able to switch from TeX to LaTeX, if you want,
1155 without breaking something)
1156 - preamble expressions must not create any dvi output
1157 - args might contain texmessage instances"""
1158 if self
.texdone
or not self
.preamblemode
:
1159 raise RuntimeError("preamble calls disabled due to previous text calls")
1160 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmessagesdefaultpreamble
+ texmessages
1161 self
.execute(expr
, texmessages
)
1162 self
.preambles
.append((expr
, texmessages
))
1164 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:")
1166 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[]):
1167 """create text by passing expr to TeX/LaTeX
1168 - returns a textbox containing the result from running expr thru TeX/LaTeX
1169 - the box center is set to x, y
1170 - *args may contain attr parameters, namely:
1171 - textattr instances
1172 - texmessage instances
1173 - trafo._trafo instances
1174 - style.fillstyle instances"""
1176 raise ValueError("None expression is invalid")
1178 self
.reset(reinit
=1)
1180 if self
.preamblemode
:
1181 if self
.mode
== "latex":
1182 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1183 self
.preamblemode
= 0
1185 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1186 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1187 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1188 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1189 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1190 # reverse loop over the merged textattrs (last is applied first)
1191 lentextattrs
= len(textattrs
)
1192 for i
in range(lentextattrs
):
1193 expr
= textattrs
[lentextattrs
-1-i
].apply(expr
)
1194 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1197 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1198 match
= self
.PyXBoxPattern
.search(self
.texmessage
)
1199 if not match
or int(match
.group("page")) != self
.page
:
1200 raise TexResultError("box extents not found", self
)
1201 left
, right
, height
, depth
= [float(xxx
)*72/72.27*unit
.x_pt
for xxx
in match
.group("lt", "rt", "ht", "dp")]
1202 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.finishdvi
, fillstyles
)
1206 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0]))
1208 self
.needdvitextboxes
.append(box
)
1211 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1212 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1214 PyXVariableBoxPattern
= re
.compile(r
"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1216 def textboxes(self
, text
, pageshapes
):
1217 # this is some experimental code to put text into several boxes
1218 # while the bounding shape changes from box to box (rectangles only)
1219 # first we load sev.tex
1220 if not self
.textboxesincluded
:
1221 self
.execute(r
"\input textboxes.tex", [texmessage
.load
])
1222 self
.textboxesincluded
= 1
1223 # define page shapes
1224 pageshapes_str
= "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit
.topt(pageshapes
[0][0]), 72.27/72*unit
.topt(pageshapes
[0][1]))
1225 pageshapes_str
+= "\\lohsizes={%\n"
1226 for hsize
, vsize
in pageshapes
[1:]:
1227 pageshapes_str
+= "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(hsize
))
1228 pageshapes_str
+= "{\\relax}%\n}%\n"
1229 pageshapes_str
+= "\\lovsizes={%\n"
1230 for hsize
, vsize
in pageshapes
[1:]:
1231 pageshapes_str
+= "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(vsize
))
1232 pageshapes_str
+= "{\\relax}%\n}%\n"
1238 self
.execute(pageshapes_str
, [])
1239 parnos_str
= "}{".join(parnos
)
1241 parnos_str
= "{%s}" % parnos_str
1242 parnos_str
= "\\parnos={%s{\\relax}}%%\n" % parnos_str
1243 self
.execute(parnos_str
, [])
1244 parshapes_str
= "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes
)
1245 self
.execute(parshapes_str
, [])
1246 self
.execute("\\global\\count0=1%%\n"
1247 "\\global\\parno=0%%\n"
1248 "\\global\\myprevgraf=0%%\n"
1249 "\\global\\showprevgraf=0%%\n"
1250 "\\global\\outputtype=0%%\n"
1251 "\\global\\leastcost=10000000%%\n"
1253 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1255 if self
.dvifile
is None:
1256 self
.dvifile
= dvifile
.dvifile("%s.dvi" % self
.texfilename
, self
.fontmap
, debug
=self
.dvidebug
)
1258 raise RuntimeError("textboxes currently needs texipc")
1261 lastparshapes
= parshapes
1264 lastpar
= prevgraf
= -1
1265 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
1268 page
= int(m
.group("page"))
1269 assert page
== pages
1270 par
= int(m
.group("par"))
1271 prevgraf
= int(m
.group("prevgraf"))
1272 if page
<= len(pageshapes
):
1273 width
= 72.27/72*unit
.topt(pageshapes
[page
-1][0])
1275 width
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1276 if page
< len(pageshapes
):
1277 nextwidth
= 72.27/72*unit
.topt(pageshapes
[page
][0])
1279 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1282 # a new paragraph is to be broken
1283 parnos
.append(str(par
))
1284 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1286 parshape
= " 0pt " + parshape
1287 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1288 elif prevgraf
== lastprevgraf
:
1291 # we have to append the breaking of the previous paragraph
1292 oldparshape
= " ".join(parshapes
[-1].split(' ')[2:2+2*lastprevgraf
])
1293 oldparshape
= oldparshape
.split('}')[0]
1295 oldparshape
= " " + oldparshape
1296 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1298 parshape
= " 0pt " + parshape
1301 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1303 lastprevgraf
= prevgraf
1305 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
1307 for i
in range(pages
):
1308 result
.append(self
.dvifile
.readpage([i
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1309 if parnos
== lastparnos
and parshapes
== lastparshapes
:
1313 raise TexResultError("Too many loops in textboxes ", texrunner
)
1316 # the module provides an default texrunner and methods for direct access
1317 defaulttexrunner
= texrunner()
1318 reset
= defaulttexrunner
.reset
1319 set = defaulttexrunner
.set
1320 preamble
= defaulttexrunner
.preamble
1321 text
= defaulttexrunner
.text
1322 text_pt
= defaulttexrunner
.text_pt
1324 def escapestring(s
):
1325 """escape special TeX/LaTeX characters
1327 Returns a string, where some special characters of standard
1328 TeX/LaTeX are replaced by appropriate escaped versions. Note
1329 that we cannot handle the three ASCII characters '{', '}',
1330 and '\' that way, since they do not occure in the TeX default
1331 encoding and thus are more likely to need some special handling.
1332 All other ASCII characters should usually (but not always)
1335 # ASCII strings only
1340 s
= s
[:i
] + "\\" + s
[i
:]
1343 s
= s
[:i
] + r
"\string" + s
[i
:]