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