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