create exception description asap to output the proper description (this bug was...
[PyX.git] / pyx / text.py
blob4441c8c035c4f9795cd96602929d4fde841415ec
1 #!/usr/bin/env python
2 # -*- coding: ISO-8859-1 -*-
5 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
6 # Copyright (C) 2003-2004 Michael Schindler <m-schindler@users.sourceforge.net>
7 # Copyright (C) 2002-2005 André Wobst <wobsta@users.sourceforge.net>
9 # This file is part of PyX (http://pyx.sourceforge.net/).
11 # PyX is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
16 # PyX is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
21 # You should have received a copy of the GNU General Public License
22 # along with PyX; if not, write to the Free Software
23 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
25 import glob, os, threading, Queue, re, tempfile, atexit, time, warnings
26 import config, siteconfig, unit, box, canvas, trafo, version, attr, style, dvifile
28 ###############################################################################
29 # texmessages
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)
45 prints a detailed report about the problem
46 - the verbose level is controlled by texrunner.errordebug"""
48 def __init__(self, description, texrunner):
49 if texrunner.errordebug >= 2:
50 self.description = ("%s\n" % description +
51 "The expression passed to TeX was:\n"
52 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
53 "The return message from TeX was:\n"
54 " %s\n" % texrunner.texmessage.replace("\n", "\n ").rstrip() +
55 "After parsing this message, the following was left:\n"
56 " %s" % texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
57 elif texrunner.errordebug == 1:
58 firstlines = texrunner.texmessageparsed.split("\n")
59 if len(firstlines) > 5:
60 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
61 self.description = ("%s\n" % description +
62 "The expression passed to TeX was:\n"
63 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
64 "After parsing the return message from TeX, the following was left:\n" +
65 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
66 else:
67 self.description = description
69 def __str__(self):
70 return self.description
73 class _Itexmessage:
74 """validates/invalidates TeX/LaTeX response"""
76 def check(self, texrunner):
77 """check a Tex/LaTeX response and respond appropriate
78 - read the texrunners texmessageparsed attribute
79 - if there is an problem found, raise TexResultError
80 - remove any valid and identified TeX/LaTeX response
81 from the texrunners texmessageparsed attribute
82 -> finally, there should be nothing left in there,
83 otherwise it is interpreted as an error"""
86 class texmessage(attr.attr): pass
89 class _texmessagestart(texmessage):
90 """validates TeX/LaTeX startup"""
92 __implements__ = _Itexmessage
94 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
96 def check(self, texrunner):
97 # check for "This is e-TeX"
98 m = self.startpattern.search(texrunner.texmessageparsed)
99 if not m:
100 raise TexResultError("TeX startup failed", texrunner)
101 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
103 # check for filename to be processed
104 try:
105 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
106 except (IndexError, ValueError):
107 raise TexResultError("TeX running startup file failed", texrunner)
109 # check for \raiseerror -- just to be sure that communication works
110 try:
111 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
112 except (IndexError, ValueError):
113 raise TexResultError("TeX scrollmode check failed", texrunner)
116 class _texmessagenoaux(texmessage):
117 """allows for LaTeXs no-aux-file warning"""
119 __implements__ = _Itexmessage
121 def check(self, texrunner):
122 try:
123 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
124 texrunner.texmessageparsed = s1 + s2
125 except (IndexError, ValueError):
126 try:
127 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
128 os.sep,
129 texrunner.texfilename), 1)
130 texrunner.texmessageparsed = s1 + s2
131 except (IndexError, ValueError):
132 pass
135 class _texmessageinputmarker(texmessage):
136 """validates the PyXInputMarker"""
138 __implements__ = _Itexmessage
140 def check(self, texrunner):
141 try:
142 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
143 texrunner.texmessageparsed = s1 + s2
144 except (IndexError, ValueError):
145 raise TexResultError("PyXInputMarker expected", texrunner)
148 class _texmessagepyxbox(texmessage):
149 """validates the PyXBox output"""
151 __implements__ = _Itexmessage
153 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:")
155 def check(self, texrunner):
156 m = self.pattern.search(texrunner.texmessageparsed)
157 if m and m.group("page") == str(texrunner.page):
158 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
159 else:
160 raise TexResultError("PyXBox expected", texrunner)
163 class _texmessagepyxpageout(texmessage):
164 """validates the dvi shipout message (writing a page to the dvi file)"""
166 __implements__ = _Itexmessage
168 def check(self, texrunner):
169 try:
170 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
171 texrunner.texmessageparsed = s1 + s2
172 except (IndexError, ValueError):
173 raise TexResultError("PyXPageOutMarker expected", texrunner)
176 class _texmessageend(texmessage):
177 """validates TeX/LaTeX finish"""
179 __implements__ = _Itexmessage
181 def check(self, texrunner):
182 try:
183 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
184 texrunner.texmessageparsed = s1 + s2
185 except (IndexError, ValueError):
186 try:
187 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
188 os.sep,
189 texrunner.texfilename), 1)
190 texrunner.texmessageparsed = s1 + s2
191 except (IndexError, ValueError):
192 pass
194 # check for "(see the transcript file for additional information)"
195 try:
196 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
197 texrunner.texmessageparsed = s1 + s2
198 except (IndexError, ValueError):
199 pass
201 # check for "Output written on ...dvi (1 page, 220 bytes)."
202 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
203 m = dvipattern.search(texrunner.texmessageparsed)
204 if texrunner.page:
205 if not m:
206 raise TexResultError("TeX dvifile messages expected", texrunner)
207 if m.group("page") != str(texrunner.page):
208 raise TexResultError("wrong number of pages reported", texrunner)
209 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
210 else:
211 try:
212 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 raise TexResultError("no dvifile expected", texrunner)
217 # check for "Transcript written on ...log."
218 try:
219 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
220 texrunner.texmessageparsed = s1 + s2
221 except (IndexError, ValueError):
222 raise TexResultError("TeX logfile message expected", texrunner)
225 class _texmessageemptylines(texmessage):
226 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
227 also clear TeX interactive mode warning (Please type a command or say `\\end')
230 __implements__ = _Itexmessage
232 def check(self, texrunner):
233 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
234 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
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
254 removed
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"""
258 level = 0
259 highestlevel = 0
260 res = ""
261 for c in s:
262 if c == brackets[0]:
263 level += 1
264 if level > highestlevel:
265 highestlevel = level
266 if level <= maxlevel:
267 res += c
268 if c == brackets[1]:
269 level -= 1
270 if level == 0 and highestlevel > 0:
271 return res
273 def check(self, texrunner):
274 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
275 if lowestbracketlevel is not None:
276 m = self.pattern.search(lowestbracketlevel)
277 while m:
278 filename = m.group("filename").replace("\n", "")
279 try:
280 additional = m.group("additional")
281 except IndexError:
282 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():]
286 else:
287 break
288 m = self.pattern.search(lowestbracketlevel)
289 else:
290 texrunner.texmessageparsed = lowestbracketlevel
293 class _texmessageloaddef(_texmessageload):
294 """validates the inclusion of font description files (fd-files)
295 - works like _texmessageload
296 - filename must end with .def or .fd and no further text is allowed"""
298 pattern = re.compile(r"\((?P<filename>[^)]+(\.fd|\.def))\)")
300 def baselevels(self, s, **kwargs):
301 return s
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):
312 return s
315 class _texmessageignore(_texmessageload):
316 """validates any TeX/LaTeX response
317 - this might be used, when the expression is ok, but no suitable texmessage
318 parser is available
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.loaddef = _texmessageloaddef()
333 texmessage.graphicsload = _texmessagegraphicsload()
334 texmessage.ignore = _texmessageignore()
336 # for internal use:
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)
363 while m:
364 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
365 if self.warning:
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 ###############################################################################
375 # textattrs
376 ###############################################################################
378 _textattrspreamble = ""
380 class textattr:
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%
391 \parfillskip=0pt%
392 \spaceskip=0.333em%
393 \xspaceskip=0.5em%
394 \pretolerance=9999%
395 \tolerance=9999%
396 \parindent=0pt%
397 \hyphenpenalty=9999%
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):
454 "math mode"
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):
464 "phantom text"
466 def apply(self, expr):
467 return r"\phantom{%s}" % expr
469 phantom = _phantom()
470 clearphantom = attr.clearclass(_phantom)
473 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
475 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
477 top = 1
478 middle = 2
479 bottom = 3
481 def __init__(self, width, baseline=top):
482 self.width = width * 72.27 / (unit.scale["x"] * 72)
483 self.baseline = baseline
484 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
486 def apply(self, expr):
487 if self.baseline == self.top:
488 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self.width, expr)
489 elif self.baseline == self.middle:
490 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)
491 elif self.baseline == self.bottom:
492 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self.width, expr)
493 else:
494 RuntimeError("invalid baseline argument")
496 parbox_pt.clear = attr.clearclass(parbox_pt)
498 class parbox(parbox_pt):
500 def __init__(self, width, **kwargs):
501 parbox_pt.__init__(self, unit.topt(width), **kwargs)
503 parbox.clear = parbox_pt.clear
506 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
508 class valign(attr.sortbeforeexclusiveattr, textattr):
510 def __init__(self, avalign):
511 self.valign = avalign
512 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
514 def apply(self, expr):
515 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)
517 valign.top = valign(0)
518 valign.middle = valign(0.5)
519 valign.bottom = valign(1)
520 valign.clear = valign.baseline = attr.clearclass(valign)
523 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
525 class _vshift(attr.sortbeforeattr, textattr):
527 def __init__(self):
528 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
530 def apply(self, expr):
531 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
533 class vshift(_vshift):
534 "vertical down shift by a fraction of a character height"
536 def __init__(self, lowerratio, heightstr="0"):
537 _vshift.__init__(self)
538 self.lowerratio = lowerratio
539 self.heightstr = heightstr
541 def setheightexpr(self):
542 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
544 class _vshiftmathaxis(_vshift):
545 "vertical down shift by the height of the math axis"
547 def setheightexpr(self):
548 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
551 vshift.bottomzero = vshift(0)
552 vshift.middlezero = vshift(0.5)
553 vshift.topzero = vshift(1)
554 vshift.mathaxis = _vshiftmathaxis()
555 vshift.clear = attr.clearclass(_vshift)
558 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
559 None, "tiny", "scriptsize", "footnotesize", "small"]
561 class size(attr.sortbeforeattr, textattr):
562 "font size"
564 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
565 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
566 raise RuntimeError("either specify sizeindex or sizename")
567 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
568 if sizeindex is not None:
569 if sizeindex >= 0 and sizeindex < sizelist.index(None):
570 self.size = sizelist[sizeindex]
571 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
572 self.size = sizelist[sizeindex]
573 else:
574 raise IndexError("index out of sizelist range")
575 else:
576 self.size = sizename
578 def apply(self, expr):
579 return r"\%s{}%s" % (self.size, expr)
581 size.tiny = size(-4)
582 size.scriptsize = size.script = size(-3)
583 size.footnotesize = size.footnote = size(-2)
584 size.small = size(-1)
585 size.normalsize = size.normal = size(0)
586 size.large = size(1)
587 size.Large = size(2)
588 size.LARGE = size(3)
589 size.huge = size(4)
590 size.Huge = size(5)
591 size.clear = attr.clearclass(size)
594 ###############################################################################
595 # texrunner
596 ###############################################################################
599 class _readpipe(threading.Thread):
600 """threaded reader of TeX/LaTeX output
601 - sets an event, when a specific string in the programs output is found
602 - sets an event, when the terminal ends"""
604 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
605 """initialize the reader
606 - pipe: file to be read from
607 - expectqueue: keeps the next InputMarker to be wait for
608 - gotevent: the "got InputMarker" event
609 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
610 - quitevent: the "end of terminal" event"""
611 threading.Thread.__init__(self)
612 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
613 self.pipe = pipe
614 self.expectqueue = expectqueue
615 self.gotevent = gotevent
616 self.gotqueue = gotqueue
617 self.quitevent = quitevent
618 self.expect = None
619 self.start()
621 def run(self):
622 """thread routine"""
623 read = self.pipe.readline() # read, what comes in
624 try:
625 self.expect = self.expectqueue.get_nowait() # read, what should be expected
626 except Queue.Empty:
627 pass
628 while len(read):
629 # universal EOL handling (convert everything into unix like EOLs)
630 # XXX is this necessary on pipes?
631 read = read.replace("\r", "").replace("\n", "") + "\n"
632 self.gotqueue.put(read) # report, whats read
633 if self.expect is not None and read.find(self.expect) != -1:
634 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
635 read = self.pipe.readline() # read again
636 try:
637 self.expect = self.expectqueue.get_nowait()
638 except Queue.Empty:
639 pass
640 # EOF reached
641 self.pipe.close()
642 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
643 raise RuntimeError("TeX/LaTeX finished unexpectedly")
644 self.quitevent.set()
647 class textbox(box.rect, canvas._canvas):
648 """basically a box.rect, but it contains a text created by the texrunner
649 - texrunner._text and texrunner.text return such an object
650 - _textbox instances can be inserted into a canvas
651 - the output is contained in a page of the dvifile available thru the texrunner"""
652 # TODO: shouldn't all boxes become canvases? how about inserts then?
654 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
656 - finishdvi is a method to be called to get the dvicanvas
657 (e.g. the finishdvi calls the setdvicanvas method)
658 - attrs are fillstyles"""
659 self.left = left
660 self.right = right
661 self.width = left + right
662 self.height = height
663 self.depth = depth
664 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
665 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
666 canvas._canvas.__init__(self)
667 self.finishdvi = finishdvi
668 self.dvicanvas = None
669 self.set(attrs)
670 self.insertdvicanvas = 0
672 def transform(self, *trafos):
673 if self.insertdvicanvas:
674 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
675 box.rect.transform(self, *trafos)
676 for trafo in trafos:
677 self.texttrafo = trafo * self.texttrafo
679 def setdvicanvas(self, dvicanvas):
680 if self.dvicanvas is not None:
681 raise RuntimeError("multiple call to setdvicanvas")
682 self.dvicanvas = dvicanvas
684 def ensuredvicanvas(self):
685 if self.dvicanvas is None:
686 self.finishdvi()
687 assert self.dvicanvas is not None, "finishdvi is broken"
688 if not self.insertdvicanvas:
689 self.insert(self.dvicanvas, [self.texttrafo])
690 self.insertdvicanvas = 1
692 def marker(self, marker):
693 self.ensuredvicanvas()
694 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
696 def processPS(self, file, writer, context, registry, bbox):
697 self.ensuredvicanvas()
698 canvas._canvas.processPS(self, file, writer, context, registry, bbox)
700 def processPDF(self, file, writer, context, registry, bbox):
701 self.ensuredvicanvas()
702 canvas._canvas.processPDF(self, file, writer, context, registry, bbox)
705 def _cleantmp(texrunner):
706 """get rid of temporary files
707 - function to be registered by atexit
708 - files contained in usefiles are kept"""
709 if texrunner.texruns: # cleanup while TeX is still running?
710 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
711 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
712 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
713 else:
714 texrunner.texinput.write("\n\\end\n")
715 texrunner.texinput.close() # close the input queue and
716 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
717 return # didn't got a quit from TeX -> we can't do much more
718 texrunner.texruns = 0
719 texrunner.texdone = 1
720 for usefile in texrunner.usefiles:
721 extpos = usefile.rfind(".")
722 try:
723 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
724 except OSError:
725 pass
726 for file in glob.glob("%s.*" % texrunner.texfilename):
727 try:
728 os.unlink(file)
729 except OSError:
730 pass
731 if texrunner.texdebug is not None:
732 try:
733 texrunner.texdebug.close()
734 texrunner.texdebug = None
735 except IOError:
736 pass
739 class _unset:
740 pass
742 class texrunner:
743 """TeX/LaTeX interface
744 - runs TeX/LaTeX expressions instantly
745 - checks TeX/LaTeX response
746 - the instance variable texmessage stores the last TeX
747 response as a string
748 - the instance variable texmessageparsed stores a parsed
749 version of texmessage; it should be empty after
750 texmessage.check was called, otherwise a TexResultError
751 is raised
752 - the instance variable errordebug controls the verbose
753 level of TexResultError"""
755 defaulttexmessagesstart = [texmessage.start]
756 defaulttexmessagesdocclass = [texmessage.load]
757 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
758 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning]
759 defaulttexmessagesdefaultpreamble = [texmessage.load]
760 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
761 texmessage.fontwarning, texmessage.boxwarning]
763 def __init__(self, mode="tex",
764 lfs="10pt",
765 docclass="article",
766 docopt=None,
767 usefiles=[],
768 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
769 waitfortex=config.getint("text", "waitfortex", 60),
770 showwaitfortex=config.getint("text", "showwaitfortex", 5),
771 texipc=config.getboolean("text", "texipc", 0),
772 texdebug=None,
773 dvidebug=0,
774 errordebug=1,
775 pyxgraphics=1,
776 texmessagesstart=[],
777 texmessagesdocclass=[],
778 texmessagesbegindoc=[],
779 texmessagesend=[],
780 texmessagesdefaultpreamble=[],
781 texmessagesdefaultrun=[]):
782 mode = mode.lower()
783 if mode != "tex" and mode != "latex":
784 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
785 self.mode = mode
786 self.lfs = lfs
787 self.docclass = docclass
788 self.docopt = docopt
789 self.usefiles = usefiles
790 self.fontmaps = fontmaps
791 self.waitfortex = waitfortex
792 self.showwaitfortex = showwaitfortex
793 self.texipc = texipc
794 if texdebug is not None:
795 if texdebug[-4:] == ".tex":
796 self.texdebug = open(texdebug, "w")
797 else:
798 self.texdebug = open("%s.tex" % texdebug, "w")
799 else:
800 self.texdebug = None
801 self.dvidebug = dvidebug
802 self.errordebug = errordebug
803 self.pyxgraphics = pyxgraphics
804 self.texmessagesstart = texmessagesstart
805 self.texmessagesdocclass = texmessagesdocclass
806 self.texmessagesbegindoc = texmessagesbegindoc
807 self.texmessagesend = texmessagesend
808 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
809 self.texmessagesdefaultrun = texmessagesdefaultrun
811 self.texruns = 0
812 self.texdone = 0
813 self.preamblemode = 1
814 self.executeid = 0
815 self.page = 0
816 self.preambles = []
817 self.needdvitextboxes = [] # when texipc-mode off
818 self.dvifile = None
819 self.textboxesincluded = 0
820 savetempdir = tempfile.tempdir
821 tempfile.tempdir = os.curdir
822 self.texfilename = os.path.basename(tempfile.mktemp())
823 tempfile.tempdir = savetempdir
825 def waitforevent(self, event):
826 """waits verbosely with an timeout for an event
827 - observes an event while periodly while printing messages
828 - returns the status of the event (isSet)
829 - does not clear the event"""
830 if self.showwaitfortex:
831 waited = 0
832 hasevent = 0
833 while waited < self.waitfortex and not hasevent:
834 if self.waitfortex - waited > self.showwaitfortex:
835 event.wait(self.showwaitfortex)
836 waited += self.showwaitfortex
837 else:
838 event.wait(self.waitfortex - waited)
839 waited += self.waitfortex - waited
840 hasevent = event.isSet()
841 if not hasevent:
842 if waited < self.waitfortex:
843 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
844 else:
845 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
846 return hasevent
847 else:
848 event.wait(self.waitfortex)
849 return event.isSet()
851 def execute(self, expr, texmessages):
852 """executes expr within TeX/LaTeX
853 - if self.texruns is not yet set, TeX/LaTeX is initialized,
854 self.texruns is set and self.preamblemode is set
855 - the method must not be called, when self.texdone is already set
856 - expr should be a string or None
857 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
858 self.texdone becomes set
859 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
860 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
861 - texmessages is a list of texmessage instances"""
862 if not self.texruns:
863 if self.texdebug is not None:
864 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
865 self.texdebug.write("%% mode: %s\n" % self.mode)
866 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
867 for usefile in self.usefiles:
868 extpos = usefile.rfind(".")
869 try:
870 os.rename(usefile, self.texfilename + usefile[extpos:])
871 except OSError:
872 pass
873 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
874 texfile.write("\\relax%\n")
875 texfile.close()
876 if self.texipc:
877 ipcflag = " --ipc"
878 else:
879 ipcflag = ""
880 try:
881 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
882 except ValueError:
883 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
884 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
885 atexit.register(_cleantmp, self)
886 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
887 self.gotevent = threading.Event() # keeps the got inputmarker event
888 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
889 self.quitevent = threading.Event() # keeps for end of terminal event
890 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
891 self.texruns = 1
892 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
893 oldpreamblemode = self.preamblemode
894 self.preamblemode = 1
895 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
896 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
897 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
898 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
899 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
900 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
901 "\\newdimen\\PyXDimenHAlignRT%\n" +
902 _textattrspreamble + # insert preambles for textattrs macros
903 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
904 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
905 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
906 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
907 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
908 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
909 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
910 "lt=\\the\\PyXDimenHAlignLT,"
911 "rt=\\the\\PyXDimenHAlignRT,"
912 "ht=\\the\\ht\\PyXBox,"
913 "dp=\\the\\dp\\PyXBox:}%\n"
914 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
915 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
916 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
917 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
918 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
919 self.defaulttexmessagesstart + self.texmessagesstart)
920 os.remove("%s.tex" % self.texfilename)
921 if self.mode == "tex":
922 if self.lfs:
923 lfserror = None
924 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
925 lfsname = self.lfs
926 else:
927 lfsname = "%s.lfs" % self.lfs
928 for fulllfsname in [lfsname,
929 os.path.join(siteconfig.lfsdir, lfsname)]:
930 try:
931 lfsfile = open(fulllfsname, "r")
932 lfsdef = lfsfile.read()
933 lfsfile.close()
934 break
935 except IOError:
936 pass
937 else:
938 lfserror = "File '%s' is not available or not readable. " % lfsname
939 else:
940 lfserror = ""
941 if lfserror is not None:
942 allfiles = (glob.glob("*.lfs") +
943 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
944 lfsnames = []
945 for f in allfiles:
946 try:
947 open(f, "r").close()
948 lfsnames.append(os.path.basename(f)[:-4])
949 except IOError:
950 pass
951 lfsnames.sort()
952 if len(lfsnames):
953 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
954 else:
955 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
956 self.execute(lfsdef, [])
957 self.execute("\\normalsize%\n", [])
958 self.execute("\\newdimen\\linewidth%\n", [])
959 elif self.mode == "latex":
960 if self.pyxgraphics:
961 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
962 try:
963 open(pyxdef, "r").close()
964 except IOError:
965 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
966 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
967 self.execute("\\makeatletter%\n"
968 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
969 "\\def\\ProcessOptions{%\n"
970 "\\def\\Gin@driver{" + pyxdef + "}%\n"
971 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
972 "\\saveProcessOptions}%\n"
973 "\\makeatother",
975 if self.docopt is not None:
976 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
977 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
978 else:
979 self.execute("\\documentclass{%s}" % self.docclass,
980 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
981 self.preamblemode = oldpreamblemode
982 self.executeid += 1
983 if expr is not None: # TeX/LaTeX should process expr
984 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
985 if self.preamblemode:
986 self.expr = ("%s%%\n" % expr +
987 "\\PyXInput{%i}%%\n" % self.executeid)
988 else:
989 self.page += 1
990 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
991 "\\PyXInput{%i}%%\n" % self.executeid)
992 else: # TeX/LaTeX should be finished
993 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
994 if self.mode == "latex":
995 self.expr = "\\end{document}%\n"
996 else:
997 self.expr = "\\end%\n"
998 if self.texdebug is not None:
999 self.texdebug.write(self.expr)
1000 self.texinput.write(self.expr)
1001 gotevent = self.waitforevent(self.gotevent)
1002 self.gotevent.clear()
1003 if expr is None and gotevent: # TeX/LaTeX should have finished
1004 self.texruns = 0
1005 self.texdone = 1
1006 self.texinput.close() # close the input queue and
1007 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1008 try:
1009 self.texmessage = ""
1010 while 1:
1011 self.texmessage += self.gotqueue.get_nowait()
1012 except Queue.Empty:
1013 pass
1014 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1015 self.texmessageparsed = self.texmessage
1016 if gotevent:
1017 if expr is not None:
1018 texmessage.inputmarker.check(self)
1019 if not self.preamblemode:
1020 texmessage.pyxbox.check(self)
1021 texmessage.pyxpageout.check(self)
1022 texmessages = attr.mergeattrs(texmessages)
1023 for t in texmessages:
1024 t.check(self)
1025 keeptexmessageparsed = self.texmessageparsed
1026 texmessage.emptylines.check(self)
1027 if len(self.texmessageparsed):
1028 self.texmessageparsed = keeptexmessageparsed
1029 raise TexResultError("unhandled TeX response (might be an error)", self)
1030 else:
1031 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1033 def finishdvi(self, ignoretail=0):
1034 """finish TeX/LaTeX and read the dvifile
1035 - this method ensures that all textboxes can access their
1036 dvicanvas"""
1037 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1038 dvifilename = "%s.dvi" % self.texfilename
1039 if not self.texipc:
1040 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1041 page = 1
1042 for box in self.needdvitextboxes:
1043 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1044 page += 1
1045 if not ignoretail and self.dvifile.readpage(None) is not None:
1046 raise RuntimeError("end of dvifile expected")
1047 self.dvifile = None
1048 self.needdvitextboxes = []
1050 def reset(self, reinit=0):
1051 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1052 if self.texruns:
1053 self.finishdvi()
1054 if self.texdebug is not None:
1055 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1056 self.executeid = 0
1057 self.page = 0
1058 self.texdone = 0
1059 if reinit:
1060 self.preamblemode = 1
1061 for expr, texmessages in self.preambles:
1062 self.execute(expr, texmessages)
1063 if self.mode == "latex":
1064 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1065 self.preamblemode = 0
1066 else:
1067 self.preambles = []
1068 self.preamblemode = 1
1070 def set(self, mode=_unset,
1071 lfs=_unset,
1072 docclass=_unset,
1073 docopt=_unset,
1074 usefiles=_unset,
1075 fontmaps=_unset,
1076 waitfortex=_unset,
1077 showwaitfortex=_unset,
1078 texipc=_unset,
1079 texdebug=_unset,
1080 dvidebug=_unset,
1081 errordebug=_unset,
1082 pyxgraphics=_unset,
1083 texmessagesstart=_unset,
1084 texmessagesdocclass=_unset,
1085 texmessagesbegindoc=_unset,
1086 texmessagesend=_unset,
1087 texmessagesdefaultpreamble=_unset,
1088 texmessagesdefaultrun=_unset):
1089 """provide a set command for TeX/LaTeX settings
1090 - TeX/LaTeX must not yet been started
1091 - especially needed for the defaultrunner, where no access to
1092 the constructor is available"""
1093 if self.texruns:
1094 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1095 if mode is not _unset:
1096 mode = mode.lower()
1097 if mode != "tex" and mode != "latex":
1098 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1099 self.mode = mode
1100 if lfs is not _unset:
1101 self.lfs = lfs
1102 if docclass is not _unset:
1103 self.docclass = docclass
1104 if docopt is not _unset:
1105 self.docopt = docopt
1106 if usefiles is not _unset:
1107 self.usefiles = usefiles
1108 if fontmaps is not _unset:
1109 self.fontmaps = fontmaps
1110 if waitfortex is not _unset:
1111 self.waitfortex = waitfortex
1112 if showwaitfortex is not _unset:
1113 self.showwaitfortex = showwaitfortex
1114 if texipc is not _unset:
1115 self.texipc = texipc
1116 if texdebug is not _unset:
1117 if self.texdebug is not None:
1118 self.texdebug.close()
1119 if texdebug[-4:] == ".tex":
1120 self.texdebug = open(texdebug, "w")
1121 else:
1122 self.texdebug = open("%s.tex" % texdebug, "w")
1123 if dvidebug is not _unset:
1124 self.dvidebug = dvidebug
1125 if errordebug is not _unset:
1126 self.errordebug = errordebug
1127 if pyxgraphics is not _unset:
1128 self.pyxgraphics = pyxgraphics
1129 if errordebug is not _unset:
1130 self.errordebug = errordebug
1131 if texmessagesstart is not _unset:
1132 self.texmessagesstart = texmessagesstart
1133 if texmessagesdocclass is not _unset:
1134 self.texmessagesdocclass = texmessagesdocclass
1135 if texmessagesbegindoc is not _unset:
1136 self.texmessagesbegindoc = texmessagesbegindoc
1137 if texmessagesend is not _unset:
1138 self.texmessagesend = texmessagesend
1139 if texmessagesdefaultpreamble is not _unset:
1140 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1141 if texmessagesdefaultrun is not _unset:
1142 self.texmessagesdefaultrun = texmessagesdefaultrun
1144 def preamble(self, expr, texmessages=[]):
1145 r"""put something into the TeX/LaTeX preamble
1146 - in LaTeX, this is done before the \begin{document}
1147 (you might use \AtBeginDocument, when you're in need for)
1148 - it is not allowed to call preamble after calling the
1149 text method for the first time (for LaTeX this is needed
1150 due to \begin{document}; in TeX it is forced for compatibility
1151 (you should be able to switch from TeX to LaTeX, if you want,
1152 without breaking something)
1153 - preamble expressions must not create any dvi output
1154 - args might contain texmessage instances"""
1155 if self.texdone or not self.preamblemode:
1156 raise RuntimeError("preamble calls disabled due to previous text calls")
1157 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1158 self.execute(expr, texmessages)
1159 self.preambles.append((expr, texmessages))
1161 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:")
1163 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1164 """create text by passing expr to TeX/LaTeX
1165 - returns a textbox containing the result from running expr thru TeX/LaTeX
1166 - the box center is set to x, y
1167 - *args may contain attr parameters, namely:
1168 - textattr instances
1169 - texmessage instances
1170 - trafo._trafo instances
1171 - style.fillstyle instances"""
1172 if expr is None:
1173 raise ValueError("None expression is invalid")
1174 if self.texdone:
1175 self.reset(reinit=1)
1176 first = 0
1177 if self.preamblemode:
1178 if self.mode == "latex":
1179 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1180 self.preamblemode = 0
1181 first = 1
1182 textattrs = attr.mergeattrs(textattrs) # perform cleans
1183 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1184 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1185 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1186 textattrs = attr.getattrs(textattrs, [textattr])
1187 # reverse loop over the merged textattrs (last is applied first)
1188 lentextattrs = len(textattrs)
1189 for i in range(lentextattrs):
1190 expr = textattrs[lentextattrs-1-i].apply(expr)
1191 try:
1192 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1193 except TexResultError:
1194 self.finishdvi(ignoretail=1)
1195 raise
1196 if self.texipc:
1197 if first:
1198 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1199 match = self.PyXBoxPattern.search(self.texmessage)
1200 if not match or int(match.group("page")) != self.page:
1201 raise TexResultError("box extents not found", self)
1202 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1203 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1204 for t in trafos:
1205 box.reltransform(t)
1206 if self.texipc:
1207 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1208 else:
1209 self.needdvitextboxes.append(box)
1210 return box
1212 def text_pt(self, x, y, expr, *args, **kwargs):
1213 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1215 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1217 def textboxes(self, text, pageshapes):
1218 # this is some experimental code to put text into several boxes
1219 # while the bounding shape changes from box to box (rectangles only)
1220 # first we load sev.tex
1221 if not self.textboxesincluded:
1222 self.execute(r"\input textboxes.tex", [texmessage.load])
1223 self.textboxesincluded = 1
1224 # define page shapes
1225 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1226 pageshapes_str += "\\lohsizes={%\n"
1227 for hsize, vsize in pageshapes[1:]:
1228 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1229 pageshapes_str += "{\\relax}%\n}%\n"
1230 pageshapes_str += "\\lovsizes={%\n"
1231 for hsize, vsize in pageshapes[1:]:
1232 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1233 pageshapes_str += "{\\relax}%\n}%\n"
1234 page = 0
1235 parnos = []
1236 parshapes = []
1237 loop = 0
1238 while 1:
1239 self.execute(pageshapes_str, [])
1240 parnos_str = "}{".join(parnos)
1241 if parnos_str:
1242 parnos_str = "{%s}" % parnos_str
1243 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1244 self.execute(parnos_str, [])
1245 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1246 self.execute(parshapes_str, [])
1247 self.execute("\\global\\count0=1%%\n"
1248 "\\global\\parno=0%%\n"
1249 "\\global\\myprevgraf=0%%\n"
1250 "\\global\\showprevgraf=0%%\n"
1251 "\\global\\outputtype=0%%\n"
1252 "\\global\\leastcost=10000000%%\n"
1253 "%s%%\n"
1254 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1255 if self.texipc:
1256 if self.dvifile is None:
1257 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1258 else:
1259 raise RuntimeError("textboxes currently needs texipc")
1260 lastparnos = parnos
1261 parnos = []
1262 lastparshapes = parshapes
1263 parshapes = []
1264 pages = 0
1265 lastpar = prevgraf = -1
1266 m = self.PyXVariableBoxPattern.search(self.texmessage)
1267 while m:
1268 pages += 1
1269 page = int(m.group("page"))
1270 assert page == pages
1271 par = int(m.group("par"))
1272 prevgraf = int(m.group("prevgraf"))
1273 if page <= len(pageshapes):
1274 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1275 else:
1276 width = 72.27/72*unit.topt(pageshapes[-1][0])
1277 if page < len(pageshapes):
1278 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1279 else:
1280 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1282 if par != lastpar:
1283 # a new paragraph is to be broken
1284 parnos.append(str(par))
1285 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1286 if len(parshape):
1287 parshape = " 0pt " + parshape
1288 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1289 elif prevgraf == lastprevgraf:
1290 pass
1291 else:
1292 # we have to append the breaking of the previous paragraph
1293 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1294 oldparshape = oldparshape.split('}')[0]
1295 if len(parshape):
1296 oldparshape = " " + oldparshape
1297 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1298 if len(parshape):
1299 parshape = " 0pt " + parshape
1300 else:
1301 parshape = " "
1302 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1303 lastpar = par
1304 lastprevgraf = prevgraf
1305 nextpos = m.end()
1306 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1307 result = []
1308 for i in range(pages):
1309 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1310 if parnos == lastparnos and parshapes == lastparshapes:
1311 return result
1312 loop += 1
1313 if loop > 100:
1314 raise TexResultError("Too many loops in textboxes ", texrunner)
1317 # the module provides an default texrunner and methods for direct access
1318 defaulttexrunner = texrunner()
1319 reset = defaulttexrunner.reset
1320 set = defaulttexrunner.set
1321 preamble = defaulttexrunner.preamble
1322 text = defaulttexrunner.text
1323 text_pt = defaulttexrunner.text_pt
1325 def escapestring(s, replace={" ": "~",
1326 "$": "\\$",
1327 "&": "\\&",
1328 "#": "\\#",
1329 "_": "\\_",
1330 "%": "\\%",
1331 "^": "\\string^",
1332 "~": "\\string~",
1333 "<": "{$<$}",
1334 ">": "{$>$}",
1335 "{": "{$\{$}",
1336 "}": "{$\}$}",
1337 "\\": "{$\setminus$}",
1338 "|": "{$\mid$}"}):
1339 "escape all ascii characters such that they are printable by TeX/LaTeX"
1340 i = 0
1341 while i < len(s):
1342 if not 32 <= ord(s[i]) < 127:
1343 raise ValueError("escapestring function handles ascii strings only")
1344 c = s[i]
1345 try:
1346 r = replace[c]
1347 except KeyError:
1348 i += 1
1349 else:
1350 s = s[:i] + r + s[i+1:]
1351 i += len(r)
1352 return s