improve the sorting of controldists a bit
[PyX/mjg.git] / pyx / text.py
blob84c63fdb2a805dc03893aa384b94c9388ba235ba
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
27 import bbox as bboxmodule
29 ###############################################################################
30 # texmessages
31 # - please don't get confused:
32 # - there is a texmessage (and a texmessageparsed) attribute within the
33 # texrunner; it contains TeX/LaTeX response from the last command execution
34 # - instances of classes derived from the class texmessage are used to
35 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
36 # attribute of a texrunner instance
37 # - the multiple usage of the name texmessage might be removed in the future
38 # - texmessage instances should implement _Itexmessage
39 ###############################################################################
41 class TexResultError(RuntimeError):
42 """specialized texrunner exception class
43 - it is raised by texmessage instances, when a texmessage indicates an error
44 - it is raised by the texrunner itself, whenever there is a texmessage left
45 after all parsing of this message (by texmessage instances)
46 prints a detailed report about the problem
47 - the verbose level is controlled by texrunner.errordebug"""
49 def __init__(self, description, texrunner):
50 if texrunner.errordebug >= 2:
51 self.description = ("%s\n" % description +
52 "The expression passed to TeX was:\n"
53 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
54 "The return message from TeX was:\n"
55 " %s\n" % texrunner.texmessage.replace("\n", "\n ").rstrip() +
56 "After parsing this message, the following was left:\n"
57 " %s" % texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
58 elif texrunner.errordebug == 1:
59 firstlines = texrunner.texmessageparsed.split("\n")
60 if len(firstlines) > 5:
61 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
62 self.description = ("%s\n" % description +
63 "The expression passed to TeX was:\n"
64 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
65 "After parsing the return message from TeX, the following was left:\n" +
66 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
67 else:
68 self.description = description
70 def __str__(self):
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 _texmessageloaddef(_texmessageload):
295 """validates the inclusion of font description files (fd-files)
296 - works like _texmessageload
297 - filename must end with .def or .fd and no further text is allowed"""
299 pattern = re.compile(r"\((?P<filename>[^)]+(\.fd|\.def))\)")
301 def baselevels(self, s, **kwargs):
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.loaddef = _texmessageloaddef()
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 \def\PyXragged{%
388 \leftskip=0pt plus \PyXFlushHAlign fil%
389 \rightskip=0pt plus 1fil%
390 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
391 \parfillskip=0pt%
392 \pretolerance=9999%
393 \tolerance=9999%
394 \parindent=0pt%
395 \hyphenpenalty=9999%
396 \exhyphenpenalty=9999}%
399 class boxhalign(attr.exclusiveattr, textattr, _localattr):
401 def __init__(self, aboxhalign):
402 self.boxhalign = aboxhalign
403 attr.exclusiveattr.__init__(self, boxhalign)
405 def apply(self, expr):
406 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
408 boxhalign.left = boxhalign(0)
409 boxhalign.center = boxhalign(0.5)
410 boxhalign.right = boxhalign(1)
411 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
414 class flushhalign(attr.exclusiveattr, textattr, _localattr):
416 def __init__(self, aflushhalign):
417 self.flushhalign = aflushhalign
418 attr.exclusiveattr.__init__(self, flushhalign)
420 def apply(self, expr):
421 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
423 flushhalign.left = flushhalign(0)
424 flushhalign.center = flushhalign(0.5)
425 flushhalign.right = flushhalign(1)
426 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
429 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
431 def __init__(self, aboxhalign, aflushhalign):
432 self.boxhalign = aboxhalign
433 self.flushhalign = aflushhalign
434 attr.exclusiveattr.__init__(self, halign)
436 def apply(self, expr):
437 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
439 halign.left = halign(0, 0)
440 halign.center = halign(0.5, 0.5)
441 halign.right = halign(1, 1)
442 halign.clear = attr.clearclass(halign)
443 halign.boxleft = boxhalign.left
444 halign.boxcenter = boxhalign.center
445 halign.boxright = boxhalign.right
446 halign.flushleft = halign.raggedright = flushhalign.left
447 halign.flushcenter = halign.raggedcenter = flushhalign.center
448 halign.flushright = halign.raggedleft = flushhalign.right
451 class _mathmode(attr.attr, textattr, _localattr):
452 "math mode"
454 def apply(self, expr):
455 return r"$\displaystyle{%s}$" % expr
457 mathmode = _mathmode()
458 clearmathmode = attr.clearclass(_mathmode)
461 class _phantom(attr.attr, textattr, _localattr):
462 "phantom text"
464 def apply(self, expr):
465 return r"\phantom{%s}" % expr
467 phantom = _phantom()
468 clearphantom = attr.clearclass(_phantom)
471 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
473 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
475 top = 1
476 middle = 2
477 bottom = 3
479 def __init__(self, width, baseline=top):
480 self.width = width * 72.27 / (unit.scale["x"] * 72)
481 self.baseline = baseline
482 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
484 def apply(self, expr):
485 if self.baseline == self.top:
486 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
487 elif self.baseline == self.middle:
488 return r"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
489 elif self.baseline == self.bottom:
490 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
491 else:
492 RuntimeError("invalid baseline argument")
494 parbox_pt.clear = attr.clearclass(parbox_pt)
496 class parbox(parbox_pt):
498 def __init__(self, width, **kwargs):
499 parbox_pt.__init__(self, unit.topt(width), **kwargs)
501 parbox.clear = parbox_pt.clear
504 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
506 class valign(attr.sortbeforeexclusiveattr, textattr):
508 def __init__(self, avalign):
509 self.valign = avalign
510 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
512 def apply(self, expr):
513 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr, 1-self.valign, self.valign)
515 valign.top = valign(0)
516 valign.middle = valign(0.5)
517 valign.bottom = valign(1)
518 valign.clear = valign.baseline = attr.clearclass(valign)
521 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
523 class _vshift(attr.sortbeforeattr, textattr):
525 def __init__(self):
526 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
528 def apply(self, expr):
529 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
531 class vshift(_vshift):
532 "vertical down shift by a fraction of a character height"
534 def __init__(self, lowerratio, heightstr="0"):
535 _vshift.__init__(self)
536 self.lowerratio = lowerratio
537 self.heightstr = heightstr
539 def setheightexpr(self):
540 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
542 class _vshiftmathaxis(_vshift):
543 "vertical down shift by the height of the math axis"
545 def setheightexpr(self):
546 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
549 vshift.bottomzero = vshift(0)
550 vshift.middlezero = vshift(0.5)
551 vshift.topzero = vshift(1)
552 vshift.mathaxis = _vshiftmathaxis()
553 vshift.clear = attr.clearclass(_vshift)
556 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
557 None, "tiny", "scriptsize", "footnotesize", "small"]
559 class size(attr.sortbeforeattr, textattr):
560 "font size"
562 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
563 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
564 raise RuntimeError("either specify sizeindex or sizename")
565 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
566 if sizeindex is not None:
567 if sizeindex >= 0 and sizeindex < sizelist.index(None):
568 self.size = sizelist[sizeindex]
569 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
570 self.size = sizelist[sizeindex]
571 else:
572 raise IndexError("index out of sizelist range")
573 else:
574 self.size = sizename
576 def apply(self, expr):
577 return r"\%s{}%s" % (self.size, expr)
579 size.tiny = size(-4)
580 size.scriptsize = size.script = size(-3)
581 size.footnotesize = size.footnote = size(-2)
582 size.small = size(-1)
583 size.normalsize = size.normal = size(0)
584 size.large = size(1)
585 size.Large = size(2)
586 size.LARGE = size(3)
587 size.huge = size(4)
588 size.Huge = size(5)
589 size.clear = attr.clearclass(size)
592 ###############################################################################
593 # texrunner
594 ###############################################################################
597 class _readpipe(threading.Thread):
598 """threaded reader of TeX/LaTeX output
599 - sets an event, when a specific string in the programs output is found
600 - sets an event, when the terminal ends"""
602 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
603 """initialize the reader
604 - pipe: file to be read from
605 - expectqueue: keeps the next InputMarker to be wait for
606 - gotevent: the "got InputMarker" event
607 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
608 - quitevent: the "end of terminal" event"""
609 threading.Thread.__init__(self)
610 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
611 self.pipe = pipe
612 self.expectqueue = expectqueue
613 self.gotevent = gotevent
614 self.gotqueue = gotqueue
615 self.quitevent = quitevent
616 self.expect = None
617 self.start()
619 def run(self):
620 """thread routine"""
621 read = self.pipe.readline() # read, what comes in
622 try:
623 self.expect = self.expectqueue.get_nowait() # read, what should be expected
624 except Queue.Empty:
625 pass
626 while len(read):
627 # universal EOL handling (convert everything into unix like EOLs)
628 # XXX is this necessary on pipes?
629 read = read.replace("\r", "").replace("\n", "") + "\n"
630 self.gotqueue.put(read) # report, whats read
631 if self.expect is not None and read.find(self.expect) != -1:
632 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
633 read = self.pipe.readline() # read again
634 try:
635 self.expect = self.expectqueue.get_nowait()
636 except Queue.Empty:
637 pass
638 # EOF reached
639 self.pipe.close()
640 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
641 raise RuntimeError("TeX/LaTeX finished unexpectedly")
642 self.quitevent.set()
645 class textbox(box.rect, canvas._canvas):
646 """basically a box.rect, but it contains a text created by the texrunner
647 - texrunner._text and texrunner.text return such an object
648 - _textbox instances can be inserted into a canvas
649 - the output is contained in a page of the dvifile available thru the texrunner"""
650 # TODO: shouldn't all boxes become canvases? how about inserts then?
652 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
654 - finishdvi is a method to be called to get the dvicanvas
655 (e.g. the finishdvi calls the setdvicanvas method)
656 - attrs are fillstyles"""
657 self.left = left
658 self.right = right
659 self.width = left + right
660 self.height = height
661 self.depth = depth
662 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
663 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
664 canvas._canvas.__init__(self)
665 self.finishdvi = finishdvi
666 self.dvicanvas = None
667 self.set(attrs)
668 self.insertdvicanvas = 0
670 def transform(self, *trafos):
671 if self.insertdvicanvas:
672 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
673 box.rect.transform(self, *trafos)
674 for trafo in trafos:
675 self.texttrafo = trafo * self.texttrafo
677 def setdvicanvas(self, dvicanvas):
678 if self.dvicanvas is not None:
679 raise RuntimeError("multiple call to setdvicanvas")
680 self.dvicanvas = dvicanvas
682 def ensuredvicanvas(self):
683 if self.dvicanvas is None:
684 self.finishdvi()
685 assert self.dvicanvas is not None, "finishdvi is broken"
686 if not self.insertdvicanvas:
687 self.insert(self.dvicanvas, [self.texttrafo])
688 self.insertdvicanvas = 1
690 def marker(self, marker):
691 self.ensuredvicanvas()
692 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
694 def processPS(self, file, writer, context, registry, bbox):
695 self.ensuredvicanvas()
696 abbox = bboxmodule.empty()
697 canvas._canvas.processPS(self, file, writer, context, registry, abbox)
698 bbox += box.rect.bbox(self)
700 def processPDF(self, file, writer, context, registry, bbox):
701 self.ensuredvicanvas()
702 abbox = bboxmodule.empty()
703 canvas._canvas.processPDF(self, file, writer, context, registry, abbox)
704 bbox += box.rect.bbox(self)
707 def _cleantmp(texrunner):
708 """get rid of temporary files
709 - function to be registered by atexit
710 - files contained in usefiles are kept"""
711 if texrunner.texruns: # cleanup while TeX is still running?
712 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
713 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
714 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
715 else:
716 texrunner.texinput.write("\n\\end\n")
717 texrunner.texinput.close() # close the input queue and
718 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
719 return # didn't got a quit from TeX -> we can't do much more
720 texrunner.texruns = 0
721 texrunner.texdone = 1
722 for usefile in texrunner.usefiles:
723 extpos = usefile.rfind(".")
724 try:
725 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
726 except OSError:
727 pass
728 for file in glob.glob("%s.*" % texrunner.texfilename):
729 try:
730 os.unlink(file)
731 except OSError:
732 pass
733 if texrunner.texdebug is not None:
734 try:
735 texrunner.texdebug.close()
736 texrunner.texdebug = None
737 except IOError:
738 pass
741 class _unset:
742 pass
744 class texrunner:
745 """TeX/LaTeX interface
746 - runs TeX/LaTeX expressions instantly
747 - checks TeX/LaTeX response
748 - the instance variable texmessage stores the last TeX
749 response as a string
750 - the instance variable texmessageparsed stores a parsed
751 version of texmessage; it should be empty after
752 texmessage.check was called, otherwise a TexResultError
753 is raised
754 - the instance variable errordebug controls the verbose
755 level of TexResultError"""
757 defaulttexmessagesstart = [texmessage.start]
758 defaulttexmessagesdocclass = [texmessage.load]
759 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
760 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning]
761 defaulttexmessagesdefaultpreamble = [texmessage.load]
762 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
763 texmessage.fontwarning, texmessage.boxwarning]
765 def __init__(self, mode="tex",
766 lfs="10pt",
767 docclass="article",
768 docopt=None,
769 usefiles=[],
770 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
771 waitfortex=config.getint("text", "waitfortex", 60),
772 showwaitfortex=config.getint("text", "showwaitfortex", 5),
773 texipc=config.getboolean("text", "texipc", 0),
774 texdebug=None,
775 dvidebug=0,
776 errordebug=1,
777 pyxgraphics=1,
778 texmessagesstart=[],
779 texmessagesdocclass=[],
780 texmessagesbegindoc=[],
781 texmessagesend=[],
782 texmessagesdefaultpreamble=[],
783 texmessagesdefaultrun=[]):
784 mode = mode.lower()
785 if mode != "tex" and mode != "latex":
786 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
787 self.mode = mode
788 self.lfs = lfs
789 self.docclass = docclass
790 self.docopt = docopt
791 self.usefiles = usefiles
792 self.fontmaps = fontmaps
793 self.waitfortex = waitfortex
794 self.showwaitfortex = showwaitfortex
795 self.texipc = texipc
796 if texdebug is not None:
797 if texdebug[-4:] == ".tex":
798 self.texdebug = open(texdebug, "w")
799 else:
800 self.texdebug = open("%s.tex" % texdebug, "w")
801 else:
802 self.texdebug = None
803 self.dvidebug = dvidebug
804 self.errordebug = errordebug
805 self.pyxgraphics = pyxgraphics
806 self.texmessagesstart = texmessagesstart
807 self.texmessagesdocclass = texmessagesdocclass
808 self.texmessagesbegindoc = texmessagesbegindoc
809 self.texmessagesend = texmessagesend
810 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
811 self.texmessagesdefaultrun = texmessagesdefaultrun
813 self.texruns = 0
814 self.texdone = 0
815 self.preamblemode = 1
816 self.executeid = 0
817 self.page = 0
818 self.preambles = []
819 self.needdvitextboxes = [] # when texipc-mode off
820 self.dvifile = None
821 self.textboxesincluded = 0
822 savetempdir = tempfile.tempdir
823 tempfile.tempdir = os.curdir
824 self.texfilename = os.path.basename(tempfile.mktemp())
825 tempfile.tempdir = savetempdir
827 def waitforevent(self, event):
828 """waits verbosely with an timeout for an event
829 - observes an event while periodly while printing messages
830 - returns the status of the event (isSet)
831 - does not clear the event"""
832 if self.showwaitfortex:
833 waited = 0
834 hasevent = 0
835 while waited < self.waitfortex and not hasevent:
836 if self.waitfortex - waited > self.showwaitfortex:
837 event.wait(self.showwaitfortex)
838 waited += self.showwaitfortex
839 else:
840 event.wait(self.waitfortex - waited)
841 waited += self.waitfortex - waited
842 hasevent = event.isSet()
843 if not hasevent:
844 if waited < self.waitfortex:
845 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
846 else:
847 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
848 return hasevent
849 else:
850 event.wait(self.waitfortex)
851 return event.isSet()
853 def execute(self, expr, texmessages):
854 """executes expr within TeX/LaTeX
855 - if self.texruns is not yet set, TeX/LaTeX is initialized,
856 self.texruns is set and self.preamblemode is set
857 - the method must not be called, when self.texdone is already set
858 - expr should be a string or None
859 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
860 self.texdone becomes set
861 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
862 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
863 - texmessages is a list of texmessage instances"""
864 if not self.texruns:
865 if self.texdebug is not None:
866 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
867 self.texdebug.write("%% mode: %s\n" % self.mode)
868 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
869 for usefile in self.usefiles:
870 extpos = usefile.rfind(".")
871 try:
872 os.rename(usefile, self.texfilename + usefile[extpos:])
873 except OSError:
874 pass
875 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
876 texfile.write("\\relax%\n")
877 texfile.close()
878 if self.texipc:
879 ipcflag = " --ipc"
880 else:
881 ipcflag = ""
882 try:
883 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
884 except ValueError:
885 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
886 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
887 atexit.register(_cleantmp, self)
888 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
889 self.gotevent = threading.Event() # keeps the got inputmarker event
890 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
891 self.quitevent = threading.Event() # keeps for end of terminal event
892 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
893 self.texruns = 1
894 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
895 oldpreamblemode = self.preamblemode
896 self.preamblemode = 1
897 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
898 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
899 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
900 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
901 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
902 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
903 "\\newdimen\\PyXDimenHAlignRT%\n" +
904 _textattrspreamble + # insert preambles for textattrs macros
905 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
906 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
907 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
908 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
909 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
910 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
911 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
912 "lt=\\the\\PyXDimenHAlignLT,"
913 "rt=\\the\\PyXDimenHAlignRT,"
914 "ht=\\the\\ht\\PyXBox,"
915 "dp=\\the\\dp\\PyXBox:}%\n"
916 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
917 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
918 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
919 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
920 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
921 self.defaulttexmessagesstart + self.texmessagesstart)
922 os.remove("%s.tex" % self.texfilename)
923 if self.mode == "tex":
924 if self.lfs:
925 lfserror = None
926 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
927 lfsname = self.lfs
928 else:
929 lfsname = "%s.lfs" % self.lfs
930 for fulllfsname in [lfsname,
931 os.path.join(siteconfig.lfsdir, lfsname)]:
932 try:
933 lfsfile = open(fulllfsname, "r")
934 lfsdef = lfsfile.read()
935 lfsfile.close()
936 break
937 except IOError:
938 pass
939 else:
940 lfserror = "File '%s' is not available or not readable. " % lfsname
941 else:
942 lfserror = ""
943 if lfserror is not None:
944 allfiles = (glob.glob("*.lfs") +
945 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
946 lfsnames = []
947 for f in allfiles:
948 try:
949 open(f, "r").close()
950 lfsnames.append(os.path.basename(f)[:-4])
951 except IOError:
952 pass
953 lfsnames.sort()
954 if len(lfsnames):
955 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
956 else:
957 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
958 self.execute(lfsdef, [])
959 self.execute("\\normalsize%\n", [])
960 self.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
961 elif self.mode == "latex":
962 if self.pyxgraphics:
963 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
964 try:
965 open(pyxdef, "r").close()
966 except IOError:
967 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
968 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
969 self.execute("\\makeatletter%\n"
970 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
971 "\\def\\ProcessOptions{%\n"
972 "\\def\\Gin@driver{" + pyxdef + "}%\n"
973 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
974 "\\saveProcessOptions}%\n"
975 "\\makeatother",
977 if self.docopt is not None:
978 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
979 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
980 else:
981 self.execute("\\documentclass{%s}" % self.docclass,
982 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
983 self.preamblemode = oldpreamblemode
984 self.executeid += 1
985 if expr is not None: # TeX/LaTeX should process expr
986 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
987 if self.preamblemode:
988 self.expr = ("%s%%\n" % expr +
989 "\\PyXInput{%i}%%\n" % self.executeid)
990 else:
991 self.page += 1
992 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
993 "\\PyXInput{%i}%%\n" % self.executeid)
994 else: # TeX/LaTeX should be finished
995 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
996 if self.mode == "latex":
997 self.expr = "\\end{document}%\n"
998 else:
999 self.expr = "\\end%\n"
1000 if self.texdebug is not None:
1001 self.texdebug.write(self.expr)
1002 self.texinput.write(self.expr)
1003 gotevent = self.waitforevent(self.gotevent)
1004 self.gotevent.clear()
1005 if expr is None and gotevent: # TeX/LaTeX should have finished
1006 self.texruns = 0
1007 self.texdone = 1
1008 self.texinput.close() # close the input queue and
1009 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1010 try:
1011 self.texmessage = ""
1012 while 1:
1013 self.texmessage += self.gotqueue.get_nowait()
1014 except Queue.Empty:
1015 pass
1016 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1017 self.texmessageparsed = self.texmessage
1018 if gotevent:
1019 if expr is not None:
1020 texmessage.inputmarker.check(self)
1021 if not self.preamblemode:
1022 texmessage.pyxbox.check(self)
1023 texmessage.pyxpageout.check(self)
1024 texmessages = attr.mergeattrs(texmessages)
1025 for t in texmessages:
1026 t.check(self)
1027 keeptexmessageparsed = self.texmessageparsed
1028 texmessage.emptylines.check(self)
1029 if len(self.texmessageparsed):
1030 self.texmessageparsed = keeptexmessageparsed
1031 raise TexResultError("unhandled TeX response (might be an error)", self)
1032 else:
1033 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1035 def finishdvi(self, ignoretail=0):
1036 """finish TeX/LaTeX and read the dvifile
1037 - this method ensures that all textboxes can access their
1038 dvicanvas"""
1039 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1040 dvifilename = "%s.dvi" % self.texfilename
1041 if not self.texipc:
1042 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1043 page = 1
1044 for box in self.needdvitextboxes:
1045 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1046 page += 1
1047 if not ignoretail and self.dvifile.readpage(None) is not None:
1048 raise RuntimeError("end of dvifile expected")
1049 self.dvifile = None
1050 self.needdvitextboxes = []
1052 def reset(self, reinit=0):
1053 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1054 if self.texruns:
1055 self.finishdvi()
1056 if self.texdebug is not None:
1057 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1058 self.executeid = 0
1059 self.page = 0
1060 self.texdone = 0
1061 if reinit:
1062 self.preamblemode = 1
1063 for expr, texmessages in self.preambles:
1064 self.execute(expr, texmessages)
1065 if self.mode == "latex":
1066 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1067 self.preamblemode = 0
1068 else:
1069 self.preambles = []
1070 self.preamblemode = 1
1072 def set(self, mode=_unset,
1073 lfs=_unset,
1074 docclass=_unset,
1075 docopt=_unset,
1076 usefiles=_unset,
1077 fontmaps=_unset,
1078 waitfortex=_unset,
1079 showwaitfortex=_unset,
1080 texipc=_unset,
1081 texdebug=_unset,
1082 dvidebug=_unset,
1083 errordebug=_unset,
1084 pyxgraphics=_unset,
1085 texmessagesstart=_unset,
1086 texmessagesdocclass=_unset,
1087 texmessagesbegindoc=_unset,
1088 texmessagesend=_unset,
1089 texmessagesdefaultpreamble=_unset,
1090 texmessagesdefaultrun=_unset):
1091 """provide a set command for TeX/LaTeX settings
1092 - TeX/LaTeX must not yet been started
1093 - especially needed for the defaultrunner, where no access to
1094 the constructor is available"""
1095 if self.texruns:
1096 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1097 if mode is not _unset:
1098 mode = mode.lower()
1099 if mode != "tex" and mode != "latex":
1100 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1101 self.mode = mode
1102 if lfs is not _unset:
1103 self.lfs = lfs
1104 if docclass is not _unset:
1105 self.docclass = docclass
1106 if docopt is not _unset:
1107 self.docopt = docopt
1108 if usefiles is not _unset:
1109 self.usefiles = usefiles
1110 if fontmaps is not _unset:
1111 self.fontmaps = fontmaps
1112 if waitfortex is not _unset:
1113 self.waitfortex = waitfortex
1114 if showwaitfortex is not _unset:
1115 self.showwaitfortex = showwaitfortex
1116 if texipc is not _unset:
1117 self.texipc = texipc
1118 if texdebug is not _unset:
1119 if self.texdebug is not None:
1120 self.texdebug.close()
1121 if texdebug[-4:] == ".tex":
1122 self.texdebug = open(texdebug, "w")
1123 else:
1124 self.texdebug = open("%s.tex" % texdebug, "w")
1125 if dvidebug is not _unset:
1126 self.dvidebug = dvidebug
1127 if errordebug is not _unset:
1128 self.errordebug = errordebug
1129 if pyxgraphics is not _unset:
1130 self.pyxgraphics = pyxgraphics
1131 if errordebug is not _unset:
1132 self.errordebug = errordebug
1133 if texmessagesstart is not _unset:
1134 self.texmessagesstart = texmessagesstart
1135 if texmessagesdocclass is not _unset:
1136 self.texmessagesdocclass = texmessagesdocclass
1137 if texmessagesbegindoc is not _unset:
1138 self.texmessagesbegindoc = texmessagesbegindoc
1139 if texmessagesend is not _unset:
1140 self.texmessagesend = texmessagesend
1141 if texmessagesdefaultpreamble is not _unset:
1142 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1143 if texmessagesdefaultrun is not _unset:
1144 self.texmessagesdefaultrun = texmessagesdefaultrun
1146 def preamble(self, expr, texmessages=[]):
1147 r"""put something into the TeX/LaTeX preamble
1148 - in LaTeX, this is done before the \begin{document}
1149 (you might use \AtBeginDocument, when you're in need for)
1150 - it is not allowed to call preamble after calling the
1151 text method for the first time (for LaTeX this is needed
1152 due to \begin{document}; in TeX it is forced for compatibility
1153 (you should be able to switch from TeX to LaTeX, if you want,
1154 without breaking something)
1155 - preamble expressions must not create any dvi output
1156 - args might contain texmessage instances"""
1157 if self.texdone or not self.preamblemode:
1158 raise RuntimeError("preamble calls disabled due to previous text calls")
1159 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1160 self.execute(expr, texmessages)
1161 self.preambles.append((expr, texmessages))
1163 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:")
1165 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1166 """create text by passing expr to TeX/LaTeX
1167 - returns a textbox containing the result from running expr thru TeX/LaTeX
1168 - the box center is set to x, y
1169 - *args may contain attr parameters, namely:
1170 - textattr instances
1171 - texmessage instances
1172 - trafo._trafo instances
1173 - style.fillstyle instances"""
1174 if expr is None:
1175 raise ValueError("None expression is invalid")
1176 if self.texdone:
1177 self.reset(reinit=1)
1178 first = 0
1179 if self.preamblemode:
1180 if self.mode == "latex":
1181 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1182 self.preamblemode = 0
1183 first = 1
1184 textattrs = attr.mergeattrs(textattrs) # perform cleans
1185 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1186 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1187 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1188 textattrs = attr.getattrs(textattrs, [textattr])
1189 # reverse loop over the merged textattrs (last is applied first)
1190 lentextattrs = len(textattrs)
1191 for i in range(lentextattrs):
1192 expr = textattrs[lentextattrs-1-i].apply(expr)
1193 try:
1194 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1195 except TexResultError:
1196 self.finishdvi(ignoretail=1)
1197 raise
1198 if self.texipc:
1199 if first:
1200 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1201 match = self.PyXBoxPattern.search(self.texmessage)
1202 if not match or int(match.group("page")) != self.page:
1203 raise TexResultError("box extents not found", self)
1204 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1205 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1206 for t in trafos:
1207 box.reltransform(t)
1208 if self.texipc:
1209 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1210 else:
1211 self.needdvitextboxes.append(box)
1212 return box
1214 def text_pt(self, x, y, expr, *args, **kwargs):
1215 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1217 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1219 def textboxes(self, text, pageshapes):
1220 # this is some experimental code to put text into several boxes
1221 # while the bounding shape changes from box to box (rectangles only)
1222 # first we load sev.tex
1223 if not self.textboxesincluded:
1224 self.execute(r"\input textboxes.tex", [texmessage.load])
1225 self.textboxesincluded = 1
1226 # define page shapes
1227 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1228 pageshapes_str += "\\lohsizes={%\n"
1229 for hsize, vsize in pageshapes[1:]:
1230 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1231 pageshapes_str += "{\\relax}%\n}%\n"
1232 pageshapes_str += "\\lovsizes={%\n"
1233 for hsize, vsize in pageshapes[1:]:
1234 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1235 pageshapes_str += "{\\relax}%\n}%\n"
1236 page = 0
1237 parnos = []
1238 parshapes = []
1239 loop = 0
1240 while 1:
1241 self.execute(pageshapes_str, [])
1242 parnos_str = "}{".join(parnos)
1243 if parnos_str:
1244 parnos_str = "{%s}" % parnos_str
1245 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1246 self.execute(parnos_str, [])
1247 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1248 self.execute(parshapes_str, [])
1249 self.execute("\\global\\count0=1%%\n"
1250 "\\global\\parno=0%%\n"
1251 "\\global\\myprevgraf=0%%\n"
1252 "\\global\\showprevgraf=0%%\n"
1253 "\\global\\outputtype=0%%\n"
1254 "\\global\\leastcost=10000000%%\n"
1255 "%s%%\n"
1256 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1257 if self.texipc:
1258 if self.dvifile is None:
1259 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1260 else:
1261 raise RuntimeError("textboxes currently needs texipc")
1262 lastparnos = parnos
1263 parnos = []
1264 lastparshapes = parshapes
1265 parshapes = []
1266 pages = 0
1267 lastpar = prevgraf = -1
1268 m = self.PyXVariableBoxPattern.search(self.texmessage)
1269 while m:
1270 pages += 1
1271 page = int(m.group("page"))
1272 assert page == pages
1273 par = int(m.group("par"))
1274 prevgraf = int(m.group("prevgraf"))
1275 if page <= len(pageshapes):
1276 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1277 else:
1278 width = 72.27/72*unit.topt(pageshapes[-1][0])
1279 if page < len(pageshapes):
1280 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1281 else:
1282 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1284 if par != lastpar:
1285 # a new paragraph is to be broken
1286 parnos.append(str(par))
1287 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1288 if len(parshape):
1289 parshape = " 0pt " + parshape
1290 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1291 elif prevgraf == lastprevgraf:
1292 pass
1293 else:
1294 # we have to append the breaking of the previous paragraph
1295 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1296 oldparshape = oldparshape.split('}')[0]
1297 if len(parshape):
1298 oldparshape = " " + oldparshape
1299 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1300 if len(parshape):
1301 parshape = " 0pt " + parshape
1302 else:
1303 parshape = " "
1304 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1305 lastpar = par
1306 lastprevgraf = prevgraf
1307 nextpos = m.end()
1308 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1309 result = []
1310 for i in range(pages):
1311 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1312 if parnos == lastparnos and parshapes == lastparshapes:
1313 return result
1314 loop += 1
1315 if loop > 100:
1316 raise TexResultError("Too many loops in textboxes ", texrunner)
1319 # the module provides an default texrunner and methods for direct access
1320 defaulttexrunner = texrunner()
1321 reset = defaulttexrunner.reset
1322 set = defaulttexrunner.set
1323 preamble = defaulttexrunner.preamble
1324 text = defaulttexrunner.text
1325 text_pt = defaulttexrunner.text_pt
1327 def escapestring(s, replace={" ": "~",
1328 "$": "\\$",
1329 "&": "\\&",
1330 "#": "\\#",
1331 "_": "\\_",
1332 "%": "\\%",
1333 "^": "\\string^",
1334 "~": "\\string~",
1335 "<": "{$<$}",
1336 ">": "{$>$}",
1337 "{": "{$\{$}",
1338 "}": "{$\}$}",
1339 "\\": "{$\setminus$}",
1340 "|": "{$\mid$}"}):
1341 "escape all ascii characters such that they are printable by TeX/LaTeX"
1342 i = 0
1343 while i < len(s):
1344 if not 32 <= ord(s[i]) < 127:
1345 raise ValueError("escapestring function handles ascii strings only")
1346 c = s[i]
1347 try:
1348 r = replace[c]
1349 except KeyError:
1350 i += 1
1351 else:
1352 s = s[:i] + r + s[i+1:]
1353 i += len(r)
1354 return s