correct the self-intersection in parallel
[PyX/mjg.git] / pyx / text.py
blobd9d90c2c405be03cfe42402e9f2e653b912e9ca5
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)"""
46 def __init__(self, description, texrunner):
47 self.description = description
48 self.texrunner = texrunner
50 def __str__(self):
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())
70 else:
71 return self.description
74 class _Itexmessage:
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)
100 if not m:
101 raise TexResultError("TeX startup failed", texrunner)
102 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
104 # check for filename to be processed
105 try:
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
111 try:
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):
123 try:
124 s1, s2 = texrunner.texmessageparsed.split("No file %s.aux." % texrunner.texfilename, 1)
125 texrunner.texmessageparsed = s1 + s2
126 except (IndexError, ValueError):
127 try:
128 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.aux." % (os.curdir,
129 os.sep,
130 texrunner.texfilename), 1)
131 texrunner.texmessageparsed = s1 + s2
132 except (IndexError, ValueError):
133 pass
136 class _texmessageinputmarker(texmessage):
137 """validates the PyXInputMarker"""
139 __implements__ = _Itexmessage
141 def check(self, texrunner):
142 try:
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():]
160 else:
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):
170 try:
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):
183 try:
184 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
185 texrunner.texmessageparsed = s1 + s2
186 except (IndexError, ValueError):
187 try:
188 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
189 os.sep,
190 texrunner.texfilename), 1)
191 texrunner.texmessageparsed = s1 + s2
192 except (IndexError, ValueError):
193 pass
195 # check for "(see the transcript file for additional information)"
196 try:
197 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
198 texrunner.texmessageparsed = s1 + s2
199 except (IndexError, ValueError):
200 pass
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)
205 if texrunner.page:
206 if not m:
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():]
211 else:
212 try:
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."
219 try:
220 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
221 texrunner.texmessageparsed = s1 + s2
222 except (IndexError, ValueError):
223 raise TexResultError("TeX logfile message expected", texrunner)
226 class _texmessageemptylines(texmessage):
227 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
228 also clear TeX interactive mode warning (Please type a command or say `\\end')
231 __implements__ = _Itexmessage
233 def check(self, texrunner):
234 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
235 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
236 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
237 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
240 class _texmessageload(texmessage):
241 """validates inclusion of arbitrary files
242 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
243 <fielname> is a readable file and other stuff can be anything
244 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
245 - this is not always wanted, but we just assume that file inclusion is fine"""
247 __implements__ = _Itexmessage
249 pattern = re.compile(r"\((?P<filename>[^()\s\n]+)(?P<additional>[^()]*)\)")
251 def baselevels(self, s, maxlevel=1, brackets="()"):
252 """strip parts of a string above a given bracket level
253 - return a modified (some parts might be removed) version of the string s
254 where all parts inside brackets with level higher than maxlevel are
255 removed
256 - if brackets do not match (number of left and right brackets is wrong
257 or at some points there were more right brackets than left brackets)
258 just return the unmodified string"""
259 level = 0
260 highestlevel = 0
261 res = ""
262 for c in s:
263 if c == brackets[0]:
264 level += 1
265 if level > highestlevel:
266 highestlevel = level
267 if level <= maxlevel:
268 res += c
269 if c == brackets[1]:
270 level -= 1
271 if level == 0 and highestlevel > 0:
272 return res
274 def check(self, texrunner):
275 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
276 if lowestbracketlevel is not None:
277 m = self.pattern.search(lowestbracketlevel)
278 while m:
279 filename = m.group("filename").replace("\n", "")
280 try:
281 additional = m.group("additional")
282 except IndexError:
283 additional = ""
284 if (os.access(filename, os.R_OK) or
285 len(additional) and additional[0] == "\n" and os.access(filename+additional.split()[0], os.R_OK)):
286 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
287 else:
288 break
289 m = self.pattern.search(lowestbracketlevel)
290 else:
291 texrunner.texmessageparsed = lowestbracketlevel
294 class _texmessageloadfd(_texmessageload):
295 """validates the inclusion of font description files (fd-files)
296 - works like _texmessageload
297 - filename must end with .fd and no further text is allowed"""
299 pattern = re.compile(r"\((?P<filename>[^)]+.fd)\)")
301 def baselevels(self, s, **kwargs):
302 return s
305 class _texmessagegraphicsload(_texmessageload):
306 """validates the inclusion of files as the graphics packages writes it
307 - works like _texmessageload, but using "<" and ">" as delimiters
308 - filename must end with .eps and no further text is allowed"""
310 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
312 def baselevels(self, s, **kwargs):
313 return s
316 class _texmessageignore(_texmessageload):
317 """validates any TeX/LaTeX response
318 - this might be used, when the expression is ok, but no suitable texmessage
319 parser is available
320 - PLEASE: - consider writing suitable tex message parsers
321 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
323 __implements__ = _Itexmessage
325 def check(self, texrunner):
326 texrunner.texmessageparsed = ""
329 texmessage.start = _texmessagestart()
330 texmessage.noaux = _texmessagenoaux()
331 texmessage.end = _texmessageend()
332 texmessage.load = _texmessageload()
333 texmessage.loadfd = _texmessageloadfd()
334 texmessage.graphicsload = _texmessagegraphicsload()
335 texmessage.ignore = _texmessageignore()
337 # for internal use:
338 texmessage.inputmarker = _texmessageinputmarker()
339 texmessage.pyxbox = _texmessagepyxbox()
340 texmessage.pyxpageout = _texmessagepyxpageout()
341 texmessage.emptylines = _texmessageemptylines()
344 class _texmessageallwarning(texmessage):
345 """validates a given pattern 'pattern' as a warning 'warning'"""
347 def check(self, texrunner):
348 if texrunner.texmessageparsed:
349 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
350 texrunner.texmessageparsed = ""
352 texmessage.allwarning = _texmessageallwarning()
355 class texmessagepattern(texmessage):
356 """validates a given pattern and issue a warning (when set)"""
358 def __init__(self, pattern, warning=None):
359 self.pattern = pattern
360 self.warning = warning
362 def check(self, texrunner):
363 m = self.pattern.search(texrunner.texmessageparsed)
364 while m:
365 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
366 if self.warning:
367 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
368 m = self.pattern.search(texrunner.texmessageparsed)
370 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "ignoring font warning")
371 texmessage.boxwarning = texmessagepattern(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "ignoring overfull/underfull box warning")
375 ###############################################################################
376 # textattrs
377 ###############################################################################
379 _textattrspreamble = ""
381 class textattr:
382 "a textattr defines a apply method, which modifies a (La)TeX expression"
384 class _localattr: pass
386 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
387 \newdimen\PyXraggedskipplus%
388 \def\PyXragged{\PyXraggedskipplus=4em%
389 \leftskip=0pt plus \PyXFlushHAlign\PyXraggedskipplus%
390 \rightskip=0pt plus \PyXraggedskipplus%
391 \advance\rightskip0pt plus -\PyXFlushHAlign\PyXraggedskipplus%
392 \parfillskip=0pt%
393 \spaceskip=0.333em%
394 \xspaceskip=0.5em%
395 \pretolerance=9999%
396 \tolerance=9999%
397 \parindent=0pt%
398 \hyphenpenalty=9999%
399 \exhyphenpenalty=9999}%
402 class boxhalign(attr.exclusiveattr, textattr, _localattr):
404 def __init__(self, aboxhalign):
405 self.boxhalign = aboxhalign
406 attr.exclusiveattr.__init__(self, boxhalign)
408 def apply(self, expr):
409 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
411 boxhalign.left = boxhalign(0)
412 boxhalign.center = boxhalign(0.5)
413 boxhalign.right = boxhalign(1)
414 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
417 class flushhalign(attr.exclusiveattr, textattr, _localattr):
419 def __init__(self, aflushhalign):
420 self.flushhalign = aflushhalign
421 attr.exclusiveattr.__init__(self, flushhalign)
423 def apply(self, expr):
424 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
426 flushhalign.left = flushhalign(0)
427 flushhalign.center = flushhalign(0.5)
428 flushhalign.right = flushhalign(1)
429 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
432 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
434 def __init__(self, aboxhalign, aflushhalign):
435 self.boxhalign = aboxhalign
436 self.flushhalign = aflushhalign
437 attr.exclusiveattr.__init__(self, halign)
439 def apply(self, expr):
440 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
442 halign.left = halign(0, 0)
443 halign.center = halign(0.5, 0.5)
444 halign.right = halign(1, 1)
445 halign.clear = attr.clearclass(halign)
446 halign.boxleft = boxhalign.left
447 halign.boxcenter = boxhalign.center
448 halign.boxright = boxhalign.right
449 halign.flushleft = halign.raggedright = flushhalign.left
450 halign.flushcenter = halign.raggedcenter = flushhalign.center
451 halign.flushright = halign.raggedleft = flushhalign.right
454 class _mathmode(attr.attr, textattr, _localattr):
455 "math mode"
457 def apply(self, expr):
458 return r"$\displaystyle{%s}$" % expr
460 mathmode = _mathmode()
461 clearmathmode = attr.clearclass(_mathmode)
464 class _phantom(attr.attr, textattr, _localattr):
465 "phantom text"
467 def apply(self, expr):
468 return r"\phantom{%s}" % expr
470 phantom = _phantom()
471 clearphantom = attr.clearclass(_phantom)
474 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
476 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
478 top = 1
479 middle = 2
480 bottom = 3
482 def __init__(self, width, baseline=top):
483 self.width = width * 72.27 / (unit.scale["x"] * 72)
484 self.baseline = baseline
485 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
487 def apply(self, expr):
488 if self.baseline == self.top:
489 return r"\linewidth%.5ftruept\vtop{\hsize\linewidth{}%s}" % (self.width, expr)
490 elif self.baseline == self.middle:
491 return r"\linewidth%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
492 elif self.baseline == self.bottom:
493 return r"\linewidth%.5ftruept\vbox{\hsize\linewidth{}%s}" % (self.width, expr)
494 else:
495 RuntimeError("invalid baseline argument")
497 parbox_pt.clear = attr.clearclass(parbox_pt)
499 class parbox(parbox_pt):
501 def __init__(self, width, **kwargs):
502 parbox_pt.__init__(self, unit.topt(width), **kwargs)
504 parbox.clear = parbox_pt.clear
507 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
509 class valign(attr.sortbeforeexclusiveattr, textattr):
511 def __init__(self, avalign):
512 self.valign = avalign
513 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
515 def apply(self, expr):
516 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr, 1-self.valign, self.valign)
518 valign.top = valign(0)
519 valign.middle = valign(0.5)
520 valign.bottom = valign(1)
521 valign.clear = valign.baseline = attr.clearclass(valign)
524 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
526 class _vshift(attr.sortbeforeattr, textattr):
528 def __init__(self):
529 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
531 def apply(self, expr):
532 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
534 class vshift(_vshift):
535 "vertical down shift by a fraction of a character height"
537 def __init__(self, lowerratio, heightstr="0"):
538 _vshift.__init__(self)
539 self.lowerratio = lowerratio
540 self.heightstr = heightstr
542 def setheightexpr(self):
543 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
545 class _vshiftmathaxis(_vshift):
546 "vertical down shift by the height of the math axis"
548 def setheightexpr(self):
549 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
552 vshift.bottomzero = vshift(0)
553 vshift.middlezero = vshift(0.5)
554 vshift.topzero = vshift(1)
555 vshift.mathaxis = _vshiftmathaxis()
556 vshift.clear = attr.clearclass(_vshift)
559 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
560 None, "tiny", "scriptsize", "footnotesize", "small"]
562 class size(attr.sortbeforeattr, textattr):
563 "font size"
565 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
566 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
567 raise RuntimeError("either specify sizeindex or sizename")
568 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
569 if sizeindex is not None:
570 if sizeindex >= 0 and sizeindex < sizelist.index(None):
571 self.size = sizelist[sizeindex]
572 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
573 self.size = sizelist[sizeindex]
574 else:
575 raise IndexError("index out of sizelist range")
576 else:
577 self.size = sizename
579 def apply(self, expr):
580 return r"\%s{}%s" % (self.size, expr)
582 size.tiny = size(-4)
583 size.scriptsize = size.script = size(-3)
584 size.footnotesize = size.footnote = size(-2)
585 size.small = size(-1)
586 size.normalsize = size.normal = size(0)
587 size.large = size(1)
588 size.Large = size(2)
589 size.LARGE = size(3)
590 size.huge = size(4)
591 size.Huge = size(5)
592 size.clear = attr.clearclass(size)
595 ###############################################################################
596 # texrunner
597 ###############################################################################
600 class _readpipe(threading.Thread):
601 """threaded reader of TeX/LaTeX output
602 - sets an event, when a specific string in the programs output is found
603 - sets an event, when the terminal ends"""
605 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
606 """initialize the reader
607 - pipe: file to be read from
608 - expectqueue: keeps the next InputMarker to be wait for
609 - gotevent: the "got InputMarker" event
610 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
611 - quitevent: the "end of terminal" event"""
612 threading.Thread.__init__(self)
613 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
614 self.pipe = pipe
615 self.expectqueue = expectqueue
616 self.gotevent = gotevent
617 self.gotqueue = gotqueue
618 self.quitevent = quitevent
619 self.expect = None
620 self.start()
622 def run(self):
623 """thread routine"""
624 read = self.pipe.readline() # read, what comes in
625 try:
626 self.expect = self.expectqueue.get_nowait() # read, what should be expected
627 except Queue.Empty:
628 pass
629 while len(read):
630 # universal EOL handling (convert everything into unix like EOLs)
631 # XXX is this necessary on pipes?
632 read = read.replace("\r", "").replace("\n", "") + "\n"
633 self.gotqueue.put(read) # report, whats read
634 if self.expect is not None and read.find(self.expect) != -1:
635 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
636 read = self.pipe.readline() # read again
637 try:
638 self.expect = self.expectqueue.get_nowait()
639 except Queue.Empty:
640 pass
641 # EOF reached
642 self.pipe.close()
643 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
644 raise RuntimeError("TeX/LaTeX finished unexpectedly")
645 self.quitevent.set()
648 class textbox(box.rect, canvas._canvas):
649 """basically a box.rect, but it contains a text created by the texrunner
650 - texrunner._text and texrunner.text return such an object
651 - _textbox instances can be inserted into a canvas
652 - the output is contained in a page of the dvifile available thru the texrunner"""
653 # TODO: shouldn't all boxes become canvases? how about inserts then?
655 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
657 - finishdvi is a method to be called to get the dvicanvas
658 (e.g. the finishdvi calls the setdvicanvas method)
659 - attrs are fillstyles"""
660 self.left = left
661 self.right = right
662 self.width = left + right
663 self.height = height
664 self.depth = depth
665 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
666 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
667 canvas._canvas.__init__(self)
668 self.finishdvi = finishdvi
669 self.dvicanvas = None
670 self.set(attrs)
671 self.insertdvicanvas = 0
673 def transform(self, *trafos):
674 if self.insertdvicanvas:
675 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
676 box.rect.transform(self, *trafos)
677 for trafo in trafos:
678 self.texttrafo = trafo * self.texttrafo
680 def setdvicanvas(self, dvicanvas):
681 if self.dvicanvas is not None:
682 raise RuntimeError("multiple call to setdvicanvas")
683 self.dvicanvas = dvicanvas
685 def ensuredvicanvas(self):
686 if self.dvicanvas is None:
687 self.finishdvi()
688 assert self.dvicanvas is not None, "finishdvi is broken"
689 if not self.insertdvicanvas:
690 self.insert(self.dvicanvas, [self.texttrafo])
691 self.insertdvicanvas = 1
693 def marker(self, marker):
694 self.ensuredvicanvas()
695 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
697 def registerPS(self, registry):
698 self.ensuredvicanvas()
699 canvas._canvas.registerPS(self, registry)
701 def registerPDF(self, registry):
702 self.ensuredvicanvas()
703 canvas._canvas.registerPDF(self, registry)
705 def outputPS(self, file, writer, context):
706 self.ensuredvicanvas()
707 canvas._canvas.outputPS(self, file, writer, context)
709 def outputPDF(self, file, writer, context):
710 self.ensuredvicanvas()
711 canvas._canvas.outputPDF(self, file, writer, context)
714 def _cleantmp(texrunner):
715 """get rid of temporary files
716 - function to be registered by atexit
717 - files contained in usefiles are kept"""
718 if texrunner.texruns: # cleanup while TeX is still running?
719 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
720 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
721 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
722 else:
723 texrunner.texinput.write("\n\\end\n")
724 texrunner.texinput.close() # close the input queue and
725 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
726 return # didn't got a quit from TeX -> we can't do much more
727 texrunner.texruns = 0
728 texrunner.texdone = 1
729 for usefile in texrunner.usefiles:
730 extpos = usefile.rfind(".")
731 try:
732 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
733 except OSError:
734 pass
735 for file in glob.glob("%s.*" % texrunner.texfilename):
736 try:
737 os.unlink(file)
738 except OSError:
739 pass
740 if texrunner.texdebug is not None:
741 try:
742 texrunner.texdebug.close()
743 texrunner.texdebug = None
744 except IOError:
745 pass
748 class _unset:
749 pass
751 class texrunner:
752 """TeX/LaTeX interface
753 - runs TeX/LaTeX expressions instantly
754 - checks TeX/LaTeX response
755 - the instance variable texmessage stores the last TeX
756 response as a string
757 - the instance variable texmessageparsed stores a parsed
758 version of texmessage; it should be empty after
759 texmessage.check was called, otherwise a TexResultError
760 is raised
761 - the instance variable errordebug controls the verbose
762 level of TexResultError"""
764 defaulttexmessagesstart = [texmessage.start]
765 defaulttexmessagesdocclass = [texmessage.load]
766 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
767 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning]
768 defaulttexmessagesdefaultpreamble = [texmessage.load]
769 defaulttexmessagesdefaultrun = [texmessage.loadfd, texmessage.graphicsload,
770 texmessage.fontwarning, texmessage.boxwarning]
772 def __init__(self, mode="tex",
773 lfs="10pt",
774 docclass="article",
775 docopt=None,
776 usefiles=[],
777 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
778 waitfortex=config.getint("text", "waitfortex", 60),
779 showwaitfortex=config.getint("text", "showwaitfortex", 5),
780 texipc=config.getboolean("text", "texipc", 0),
781 texdebug=None,
782 dvidebug=0,
783 errordebug=1,
784 pyxgraphics=1,
785 texmessagesstart=[],
786 texmessagesdocclass=[],
787 texmessagesbegindoc=[],
788 texmessagesend=[],
789 texmessagesdefaultpreamble=[],
790 texmessagesdefaultrun=[]):
791 mode = mode.lower()
792 if mode != "tex" and mode != "latex":
793 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
794 self.mode = mode
795 self.lfs = lfs
796 self.docclass = docclass
797 self.docopt = docopt
798 self.usefiles = usefiles
799 self.fontmaps = fontmaps
800 self.waitfortex = waitfortex
801 self.showwaitfortex = showwaitfortex
802 self.texipc = texipc
803 if texdebug is not None:
804 if texdebug[-4:] == ".tex":
805 self.texdebug = open(texdebug, "w")
806 else:
807 self.texdebug = open("%s.tex" % texdebug, "w")
808 else:
809 self.texdebug = None
810 self.dvidebug = dvidebug
811 self.errordebug = errordebug
812 self.pyxgraphics = pyxgraphics
813 self.texmessagesstart = texmessagesstart
814 self.texmessagesdocclass = texmessagesdocclass
815 self.texmessagesbegindoc = texmessagesbegindoc
816 self.texmessagesend = texmessagesend
817 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
818 self.texmessagesdefaultrun = texmessagesdefaultrun
820 self.texruns = 0
821 self.texdone = 0
822 self.preamblemode = 1
823 self.executeid = 0
824 self.page = 0
825 self.preambles = []
826 self.needdvitextboxes = [] # when texipc-mode off
827 self.dvifile = None
828 self.textboxesincluded = 0
829 savetempdir = tempfile.tempdir
830 tempfile.tempdir = os.curdir
831 self.texfilename = os.path.basename(tempfile.mktemp())
832 tempfile.tempdir = savetempdir
834 def waitforevent(self, event):
835 """waits verbosely with an timeout for an event
836 - observes an event while periodly while printing messages
837 - returns the status of the event (isSet)
838 - does not clear the event"""
839 if self.showwaitfortex:
840 waited = 0
841 hasevent = 0
842 while waited < self.waitfortex and not hasevent:
843 if self.waitfortex - waited > self.showwaitfortex:
844 event.wait(self.showwaitfortex)
845 waited += self.showwaitfortex
846 else:
847 event.wait(self.waitfortex - waited)
848 waited += self.waitfortex - waited
849 hasevent = event.isSet()
850 if not hasevent:
851 if waited < self.waitfortex:
852 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
853 else:
854 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
855 return hasevent
856 else:
857 event.wait(self.waitfortex)
858 return event.isSet()
860 def execute(self, expr, texmessages):
861 """executes expr within TeX/LaTeX
862 - if self.texruns is not yet set, TeX/LaTeX is initialized,
863 self.texruns is set and self.preamblemode is set
864 - the method must not be called, when self.texdone is already set
865 - expr should be a string or None
866 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
867 self.texdone becomes set
868 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
869 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
870 - texmessages is a list of texmessage instances"""
871 if not self.texruns:
872 if self.texdebug is not None:
873 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
874 self.texdebug.write("%% mode: %s\n" % self.mode)
875 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
876 for usefile in self.usefiles:
877 extpos = usefile.rfind(".")
878 try:
879 os.rename(usefile, self.texfilename + usefile[extpos:])
880 except OSError:
881 pass
882 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
883 texfile.write("\\relax%\n")
884 texfile.close()
885 if self.texipc:
886 ipcflag = " --ipc"
887 else:
888 ipcflag = ""
889 try:
890 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
891 except ValueError:
892 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
893 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
894 atexit.register(_cleantmp, self)
895 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
896 self.gotevent = threading.Event() # keeps the got inputmarker event
897 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
898 self.quitevent = threading.Event() # keeps for end of terminal event
899 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
900 self.texruns = 1
901 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
902 oldpreamblemode = self.preamblemode
903 self.preamblemode = 1
904 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
905 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
906 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
907 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
908 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
909 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
910 "\\newdimen\\PyXDimenHAlignRT%\n" +
911 _textattrspreamble + # insert preambles for textattrs macros
912 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
913 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
914 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
915 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
916 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
917 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
918 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
919 "lt=\\the\\PyXDimenHAlignLT,"
920 "rt=\\the\\PyXDimenHAlignRT,"
921 "ht=\\the\\ht\\PyXBox,"
922 "dp=\\the\\dp\\PyXBox:}%\n"
923 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
924 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
925 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
926 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
927 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%\n", # write PyXMarker special into the dvi-file
928 self.defaulttexmessagesstart + self.texmessagesstart)
929 os.remove("%s.tex" % self.texfilename)
930 if self.mode == "tex":
931 if self.lfs:
932 lfserror = None
933 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
934 lfsname = self.lfs
935 else:
936 lfsname = "%s.lfs" % self.lfs
937 for fulllfsname in [lfsname,
938 os.path.join(siteconfig.lfsdir, lfsname)]:
939 try:
940 lfsfile = open(fulllfsname, "r")
941 lfsdef = lfsfile.read()
942 lfsfile.close()
943 break
944 except IOError:
945 pass
946 else:
947 lfserror = "File '%s' is not available or not readable. " % lfsname
948 else:
949 lfserror = ""
950 if lfserror is not None:
951 allfiles = (glob.glob("*.lfs") +
952 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
953 lfsnames = []
954 for f in allfiles:
955 try:
956 open(f, "r").close()
957 lfsnames.append(os.path.basename(f)[:-4])
958 except IOError:
959 pass
960 lfsnames.sort()
961 if len(lfsnames):
962 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
963 else:
964 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
965 self.execute(lfsdef, [])
966 self.execute("\\normalsize%\n", [])
967 self.execute("\\newdimen\\linewidth%\n", [])
968 elif self.mode == "latex":
969 if self.pyxgraphics:
970 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
971 try:
972 open(pyxdef, "r").close()
973 except IOError:
974 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
975 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
976 self.execute("\\makeatletter%\n"
977 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
978 "\\def\\ProcessOptions{%\n"
979 "\\def\\Gin@driver{" + pyxdef + "}%\n"
980 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
981 "\\saveProcessOptions}%\n"
982 "\\makeatother",
984 if self.docopt is not None:
985 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
986 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
987 else:
988 self.execute("\\documentclass{%s}" % self.docclass,
989 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
990 self.preamblemode = oldpreamblemode
991 self.executeid += 1
992 if expr is not None: # TeX/LaTeX should process expr
993 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
994 if self.preamblemode:
995 self.expr = ("%s%%\n" % expr +
996 "\\PyXInput{%i}%%\n" % self.executeid)
997 else:
998 self.page += 1
999 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
1000 "\\PyXInput{%i}%%\n" % self.executeid)
1001 else: # TeX/LaTeX should be finished
1002 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
1003 if self.mode == "latex":
1004 self.expr = "\\end{document}%\n"
1005 else:
1006 self.expr = "\\end%\n"
1007 if self.texdebug is not None:
1008 self.texdebug.write(self.expr)
1009 self.texinput.write(self.expr)
1010 gotevent = self.waitforevent(self.gotevent)
1011 self.gotevent.clear()
1012 if expr is None and gotevent: # TeX/LaTeX should have finished
1013 self.texruns = 0
1014 self.texdone = 1
1015 self.texinput.close() # close the input queue and
1016 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1017 try:
1018 self.texmessage = ""
1019 while 1:
1020 self.texmessage += self.gotqueue.get_nowait()
1021 except Queue.Empty:
1022 pass
1023 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1024 self.texmessageparsed = self.texmessage
1025 if gotevent:
1026 if expr is not None:
1027 texmessage.inputmarker.check(self)
1028 if not self.preamblemode:
1029 texmessage.pyxbox.check(self)
1030 texmessage.pyxpageout.check(self)
1031 texmessages = attr.mergeattrs(texmessages)
1032 for t in texmessages:
1033 t.check(self)
1034 keeptexmessageparsed = self.texmessageparsed
1035 texmessage.emptylines.check(self)
1036 if len(self.texmessageparsed):
1037 self.texmessageparsed = keeptexmessageparsed
1038 raise TexResultError("unhandled TeX response (might be an error)", self)
1039 else:
1040 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1042 def finishdvi(self, ignoretail=0):
1043 """finish TeX/LaTeX and read the dvifile
1044 - this method ensures that all textboxes can access their
1045 dvicanvas"""
1046 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1047 dvifilename = "%s.dvi" % self.texfilename
1048 if not self.texipc:
1049 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1050 page = 1
1051 for box in self.needdvitextboxes:
1052 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1053 page += 1
1054 if not ignoretail and self.dvifile.readpage(None) is not None:
1055 raise RuntimeError("end of dvifile expected")
1056 self.dvifile = None
1057 self.needdvitextboxes = []
1059 def reset(self, reinit=0):
1060 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1061 if self.texruns:
1062 self.finishdvi()
1063 if self.texdebug is not None:
1064 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1065 self.executeid = 0
1066 self.page = 0
1067 self.texdone = 0
1068 if reinit:
1069 self.preamblemode = 1
1070 for expr, texmessages in self.preambles:
1071 self.execute(expr, texmessages)
1072 if self.mode == "latex":
1073 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1074 self.preamblemode = 0
1075 else:
1076 self.preambles = []
1077 self.preamblemode = 1
1079 def set(self, mode=_unset,
1080 lfs=_unset,
1081 docclass=_unset,
1082 docopt=_unset,
1083 usefiles=_unset,
1084 fontmaps=_unset,
1085 waitfortex=_unset,
1086 showwaitfortex=_unset,
1087 texipc=_unset,
1088 texdebug=_unset,
1089 dvidebug=_unset,
1090 errordebug=_unset,
1091 pyxgraphics=_unset,
1092 texmessagesstart=_unset,
1093 texmessagesdocclass=_unset,
1094 texmessagesbegindoc=_unset,
1095 texmessagesend=_unset,
1096 texmessagesdefaultpreamble=_unset,
1097 texmessagesdefaultrun=_unset):
1098 """provide a set command for TeX/LaTeX settings
1099 - TeX/LaTeX must not yet been started
1100 - especially needed for the defaultrunner, where no access to
1101 the constructor is available"""
1102 if self.texruns:
1103 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1104 if mode is not _unset:
1105 mode = mode.lower()
1106 if mode != "tex" and mode != "latex":
1107 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1108 self.mode = mode
1109 if lfs is not _unset:
1110 self.lfs = lfs
1111 if docclass is not _unset:
1112 self.docclass = docclass
1113 if docopt is not _unset:
1114 self.docopt = docopt
1115 if usefiles is not _unset:
1116 self.usefiles = usefiles
1117 if fontmaps is not _unset:
1118 self.fontmaps = fontmaps
1119 if waitfortex is not _unset:
1120 self.waitfortex = waitfortex
1121 if showwaitfortex is not _unset:
1122 self.showwaitfortex = showwaitfortex
1123 if texipc is not _unset:
1124 self.texipc = texipc
1125 if texdebug is not _unset:
1126 if self.texdebug is not None:
1127 self.texdebug.close()
1128 if texdebug[-4:] == ".tex":
1129 self.texdebug = open(texdebug, "w")
1130 else:
1131 self.texdebug = open("%s.tex" % texdebug, "w")
1132 if dvidebug is not _unset:
1133 self.dvidebug = dvidebug
1134 if errordebug is not _unset:
1135 self.errordebug = errordebug
1136 if pyxgraphics is not _unset:
1137 self.pyxgraphics = pyxgraphics
1138 if errordebug is not _unset:
1139 self.errordebug = errordebug
1140 if texmessagesstart is not _unset:
1141 self.texmessagesstart = texmessagesstart
1142 if texmessagesdocclass is not _unset:
1143 self.texmessagesdocclass = texmessagesdocclass
1144 if texmessagesbegindoc is not _unset:
1145 self.texmessagesbegindoc = texmessagesbegindoc
1146 if texmessagesend is not _unset:
1147 self.texmessagesend = texmessagesend
1148 if texmessagesdefaultpreamble is not _unset:
1149 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1150 if texmessagesdefaultrun is not _unset:
1151 self.texmessagesdefaultrun = texmessagesdefaultrun
1153 def preamble(self, expr, texmessages=[]):
1154 r"""put something into the TeX/LaTeX preamble
1155 - in LaTeX, this is done before the \begin{document}
1156 (you might use \AtBeginDocument, when you're in need for)
1157 - it is not allowed to call preamble after calling the
1158 text method for the first time (for LaTeX this is needed
1159 due to \begin{document}; in TeX it is forced for compatibility
1160 (you should be able to switch from TeX to LaTeX, if you want,
1161 without breaking something)
1162 - preamble expressions must not create any dvi output
1163 - args might contain texmessage instances"""
1164 if self.texdone or not self.preamblemode:
1165 raise RuntimeError("preamble calls disabled due to previous text calls")
1166 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1167 self.execute(expr, texmessages)
1168 self.preambles.append((expr, texmessages))
1170 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:")
1172 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1173 """create text by passing expr to TeX/LaTeX
1174 - returns a textbox containing the result from running expr thru TeX/LaTeX
1175 - the box center is set to x, y
1176 - *args may contain attr parameters, namely:
1177 - textattr instances
1178 - texmessage instances
1179 - trafo._trafo instances
1180 - style.fillstyle instances"""
1181 if expr is None:
1182 raise ValueError("None expression is invalid")
1183 if self.texdone:
1184 self.reset(reinit=1)
1185 first = 0
1186 if self.preamblemode:
1187 if self.mode == "latex":
1188 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1189 self.preamblemode = 0
1190 first = 1
1191 textattrs = attr.mergeattrs(textattrs) # perform cleans
1192 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1193 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1194 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1195 textattrs = attr.getattrs(textattrs, [textattr])
1196 # reverse loop over the merged textattrs (last is applied first)
1197 lentextattrs = len(textattrs)
1198 for i in range(lentextattrs):
1199 expr = textattrs[lentextattrs-1-i].apply(expr)
1200 try:
1201 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1202 except TexResultError:
1203 self.finishdvi(ignoretail=1)
1204 raise
1205 if self.texipc:
1206 if first:
1207 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1208 match = self.PyXBoxPattern.search(self.texmessage)
1209 if not match or int(match.group("page")) != self.page:
1210 raise TexResultError("box extents not found", self)
1211 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1212 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1213 for t in trafos:
1214 box.reltransform(t)
1215 if self.texipc:
1216 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1217 else:
1218 self.needdvitextboxes.append(box)
1219 return box
1221 def text_pt(self, x, y, expr, *args, **kwargs):
1222 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1224 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1226 def textboxes(self, text, pageshapes):
1227 # this is some experimental code to put text into several boxes
1228 # while the bounding shape changes from box to box (rectangles only)
1229 # first we load sev.tex
1230 if not self.textboxesincluded:
1231 self.execute(r"\input textboxes.tex", [texmessage.load])
1232 self.textboxesincluded = 1
1233 # define page shapes
1234 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1235 pageshapes_str += "\\lohsizes={%\n"
1236 for hsize, vsize in pageshapes[1:]:
1237 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1238 pageshapes_str += "{\\relax}%\n}%\n"
1239 pageshapes_str += "\\lovsizes={%\n"
1240 for hsize, vsize in pageshapes[1:]:
1241 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1242 pageshapes_str += "{\\relax}%\n}%\n"
1243 page = 0
1244 parnos = []
1245 parshapes = []
1246 loop = 0
1247 while 1:
1248 self.execute(pageshapes_str, [])
1249 parnos_str = "}{".join(parnos)
1250 if parnos_str:
1251 parnos_str = "{%s}" % parnos_str
1252 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1253 self.execute(parnos_str, [])
1254 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1255 self.execute(parshapes_str, [])
1256 self.execute("\\global\\count0=1%%\n"
1257 "\\global\\parno=0%%\n"
1258 "\\global\\myprevgraf=0%%\n"
1259 "\\global\\showprevgraf=0%%\n"
1260 "\\global\\outputtype=0%%\n"
1261 "\\global\\leastcost=10000000%%\n"
1262 "%s%%\n"
1263 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1264 if self.texipc:
1265 if self.dvifile is None:
1266 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1267 else:
1268 raise RuntimeError("textboxes currently needs texipc")
1269 lastparnos = parnos
1270 parnos = []
1271 lastparshapes = parshapes
1272 parshapes = []
1273 pages = 0
1274 lastpar = prevgraf = -1
1275 m = self.PyXVariableBoxPattern.search(self.texmessage)
1276 while m:
1277 pages += 1
1278 page = int(m.group("page"))
1279 assert page == pages
1280 par = int(m.group("par"))
1281 prevgraf = int(m.group("prevgraf"))
1282 if page <= len(pageshapes):
1283 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1284 else:
1285 width = 72.27/72*unit.topt(pageshapes[-1][0])
1286 if page < len(pageshapes):
1287 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1288 else:
1289 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1291 if par != lastpar:
1292 # a new paragraph is to be broken
1293 parnos.append(str(par))
1294 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1295 if len(parshape):
1296 parshape = " 0pt " + parshape
1297 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1298 elif prevgraf == lastprevgraf:
1299 pass
1300 else:
1301 # we have to append the breaking of the previous paragraph
1302 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1303 oldparshape = oldparshape.split('}')[0]
1304 if len(parshape):
1305 oldparshape = " " + oldparshape
1306 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1307 if len(parshape):
1308 parshape = " 0pt " + parshape
1309 else:
1310 parshape = " "
1311 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1312 lastpar = par
1313 lastprevgraf = prevgraf
1314 nextpos = m.end()
1315 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1316 result = []
1317 for i in range(pages):
1318 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1319 if parnos == lastparnos and parshapes == lastparshapes:
1320 return result
1321 loop += 1
1322 if loop > 100:
1323 raise TexResultError("Too many loops in textboxes ", texrunner)
1326 # the module provides an default texrunner and methods for direct access
1327 defaulttexrunner = texrunner()
1328 reset = defaulttexrunner.reset
1329 set = defaulttexrunner.set
1330 preamble = defaulttexrunner.preamble
1331 text = defaulttexrunner.text
1332 text_pt = defaulttexrunner.text_pt
1334 def escapestring(s, replace={" ": "~",
1335 "$": "\\$",
1336 "&": "\\&",
1337 "#": "\\#",
1338 "_": "\\_",
1339 "%": "\\%",
1340 "^": "\\string^",
1341 "~": "\\string~",
1342 "<": "{$<$}",
1343 ">": "{$>$}",
1344 "{": "{$\{$}",
1345 "}": "{$\}$}",
1346 "\\": "{$\setminus$}",
1347 "|": "{$\mid$}"}):
1348 "escape all ascii characters such that they are printable by TeX/LaTeX"
1349 i = 0
1350 while i < len(s):
1351 if not 32 <= ord(s[i]) < 127:
1352 raise ValueError("escapestring function handles ascii strings only")
1353 c = s[i]
1354 try:
1355 r = replace[c]
1356 except KeyError:
1357 i += 1
1358 else:
1359 s = s[:i] + r + s[i+1:]
1360 i += len(r)
1361 return s