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