make warnings more explicit concerning the font they refer to (although this leads...
[PyX/mjg.git] / pyx / text.py
blobefe977d1ffafec1b6a2566b440e3c4eeb300099d
1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2007 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 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
26 from pyx.dvi import dvifile
27 import bbox as bboxmodule
29 ###############################################################################
30 # texmessages
31 # - please don't get confused:
32 # - there is a texmessage (and a texmessageparsed) attribute within the
33 # texrunner; it contains TeX/LaTeX response from the last command execution
34 # - instances of classes derived from the class texmessage are used to
35 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
36 # attribute of a texrunner instance
37 # - the multiple usage of the name texmessage might be removed in the future
38 # - texmessage instances should implement _Itexmessage
39 ###############################################################################
41 class TexResultError(RuntimeError):
42 """specialized texrunner exception class
43 - it is raised by texmessage instances, when a texmessage indicates an error
44 - it is raised by the texrunner itself, whenever there is a texmessage left
45 after all parsing of this message (by texmessage instances)
46 prints a detailed report about the problem
47 - the verbose level is controlled by texrunner.errordebug"""
49 def __init__(self, description, texrunner):
50 if texrunner.errordebug >= 2:
51 self.description = ("%s\n" % description +
52 "The expression passed to TeX was:\n"
53 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
54 "The return message from TeX was:\n"
55 " %s\n" % texrunner.texmessage.replace("\n", "\n ").rstrip() +
56 "After parsing this message, the following was left:\n"
57 " %s" % texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
58 elif texrunner.errordebug == 1:
59 firstlines = texrunner.texmessageparsed.split("\n")
60 if len(firstlines) > 5:
61 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
62 self.description = ("%s\n" % description +
63 "The expression passed to TeX was:\n"
64 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
65 "After parsing the return message from TeX, the following was left:\n" +
66 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
67 else:
68 self.description = description
70 def __str__(self):
71 return self.description
74 class _Itexmessage:
75 """validates/invalidates TeX/LaTeX response"""
77 def check(self, texrunner):
78 """check a Tex/LaTeX response and respond appropriate
79 - read the texrunners texmessageparsed attribute
80 - if there is an problem found, raise TexResultError
81 - remove any valid and identified TeX/LaTeX response
82 from the texrunners texmessageparsed attribute
83 -> finally, there should be nothing left in there,
84 otherwise it is interpreted as an error"""
87 class texmessage(attr.attr): pass
90 class _texmessagestart(texmessage):
91 """validates TeX/LaTeX startup"""
93 __implements__ = _Itexmessage
95 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
97 def check(self, texrunner):
98 # check for "This is e-TeX"
99 m = self.startpattern.search(texrunner.texmessageparsed)
100 if not m:
101 raise TexResultError("TeX startup failed", texrunner)
102 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
104 # check for filename to be processed
105 try:
106 texrunner.texmessageparsed = texrunner.texmessageparsed.split("%s.tex" % texrunner.texfilename, 1)[1]
107 except (IndexError, ValueError):
108 raise TexResultError("TeX running startup file failed", texrunner)
110 # check for \raiseerror -- just to be sure that communication works
111 try:
112 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
113 except (IndexError, ValueError):
114 raise TexResultError("TeX scrollmode check failed", texrunner)
117 class _texmessagenofile(texmessage):
118 """allows for LaTeXs no-file warning"""
120 __implements__ = _Itexmessage
122 def __init__(self, fileending):
123 self.fileending = fileending
125 def check(self, texrunner):
126 try:
127 s1, s2 = texrunner.texmessageparsed.split("No file %s.%s." % (texrunner.texfilename, self.fileending), 1)
128 texrunner.texmessageparsed = s1 + s2
129 except (IndexError, ValueError):
130 try:
131 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.%s." % (os.curdir,
132 os.sep,
133 texrunner.texfilename,
134 self.fileending), 1)
135 texrunner.texmessageparsed = s1 + s2
136 except (IndexError, ValueError):
137 pass
140 class _texmessageinputmarker(texmessage):
141 """validates the PyXInputMarker"""
143 __implements__ = _Itexmessage
145 def check(self, texrunner):
146 try:
147 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
148 texrunner.texmessageparsed = s1 + s2
149 except (IndexError, ValueError):
150 raise TexResultError("PyXInputMarker expected", texrunner)
153 class _texmessagepyxbox(texmessage):
154 """validates the PyXBox output"""
156 __implements__ = _Itexmessage
158 pattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
160 def check(self, texrunner):
161 m = self.pattern.search(texrunner.texmessageparsed)
162 if m and m.group("page") == str(texrunner.page):
163 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
164 else:
165 raise TexResultError("PyXBox expected", texrunner)
168 class _texmessagepyxpageout(texmessage):
169 """validates the dvi shipout message (writing a page to the dvi file)"""
171 __implements__ = _Itexmessage
173 def check(self, texrunner):
174 try:
175 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
176 texrunner.texmessageparsed = s1 + s2
177 except (IndexError, ValueError):
178 raise TexResultError("PyXPageOutMarker expected", texrunner)
181 class _texmessageend(texmessage):
182 """validates TeX/LaTeX finish"""
184 __implements__ = _Itexmessage
186 def check(self, texrunner):
187 try:
188 s1, s2 = texrunner.texmessageparsed.split("(%s.aux)" % texrunner.texfilename, 1)
189 texrunner.texmessageparsed = s1 + s2
190 except (IndexError, ValueError):
191 try:
192 s1, s2 = texrunner.texmessageparsed.split("(%s%s%s.aux)" % (os.curdir,
193 os.sep,
194 texrunner.texfilename), 1)
195 texrunner.texmessageparsed = s1 + s2
196 except (IndexError, ValueError):
197 pass
199 # check for "(see the transcript file for additional information)"
200 try:
201 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
202 texrunner.texmessageparsed = s1 + s2
203 except (IndexError, ValueError):
204 pass
206 # check for "Output written on ...dvi (1 page, 220 bytes)."
207 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
208 m = dvipattern.search(texrunner.texmessageparsed)
209 if texrunner.page:
210 if not m:
211 raise TexResultError("TeX dvifile messages expected", texrunner)
212 if m.group("page") != str(texrunner.page):
213 raise TexResultError("wrong number of pages reported", texrunner)
214 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
215 else:
216 try:
217 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
218 texrunner.texmessageparsed = s1 + s2
219 except (IndexError, ValueError):
220 raise TexResultError("no dvifile expected", texrunner)
222 # check for "Transcript written on ...log."
223 try:
224 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
225 texrunner.texmessageparsed = s1 + s2
226 except (IndexError, ValueError):
227 raise TexResultError("TeX logfile message expected", texrunner)
230 class _texmessageemptylines(texmessage):
231 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
232 also clear TeX interactive mode warning (Please type a command or say `\\end')
235 __implements__ = _Itexmessage
237 def check(self, texrunner):
238 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
239 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
240 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
241 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
244 class _texmessageload(texmessage):
245 """validates inclusion of arbitrary files
246 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
247 <filename> is a readable file and other stuff can be anything
248 - If the filename is enclosed in double quotes, it may contain blank space.
249 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
250 - this is not always wanted, but we just assume that file inclusion is fine"""
252 __implements__ = _Itexmessage
254 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?<!\")[^()\s\n]+(?!\"))|[^()\"\n]+)[\"]?(?P<additional>[^()]*)\)")
256 def baselevels(self, s, maxlevel=1, brackets="()"):
257 """strip parts of a string above a given bracket level
258 - return a modified (some parts might be removed) version of the string s
259 where all parts inside brackets with level higher than maxlevel are
260 removed
261 - if brackets do not match (number of left and right brackets is wrong
262 or at some points there were more right brackets than left brackets)
263 just return the unmodified string"""
264 level = 0
265 highestlevel = 0
266 res = ""
267 for c in s:
268 if c == brackets[0]:
269 level += 1
270 if level > highestlevel:
271 highestlevel = level
272 if level <= maxlevel:
273 res += c
274 if c == brackets[1]:
275 level -= 1
276 if level == 0 and highestlevel > 0:
277 return res
279 def check(self, texrunner):
280 lowestbracketlevel = self.baselevels(texrunner.texmessageparsed)
281 if lowestbracketlevel is not None:
282 m = self.pattern.search(lowestbracketlevel)
283 while m:
284 filename = m.group("filename").replace("\n", "")
285 try:
286 additional = m.group("additional")
287 except IndexError:
288 additional = ""
289 if (os.access(filename, os.R_OK) or
290 len(additional) and additional[0] == "\n" and os.access(filename+additional.split()[0], os.R_OK)):
291 lowestbracketlevel = lowestbracketlevel[:m.start()] + lowestbracketlevel[m.end():]
292 else:
293 break
294 m = self.pattern.search(lowestbracketlevel)
295 else:
296 texrunner.texmessageparsed = lowestbracketlevel
299 class _texmessageloaddef(_texmessageload):
300 """validates the inclusion of font description files (fd-files)
301 - works like _texmessageload
302 - filename must end with .def or .fd and no further text is allowed"""
304 pattern = re.compile(r"\((?P<filename>[^)]+(\.fd|\.def))\)")
306 def baselevels(self, s, **kwargs):
307 return s
310 class _texmessagegraphicsload(_texmessageload):
311 """validates the inclusion of files as the graphics packages writes it
312 - works like _texmessageload, but using "<" and ">" as delimiters
313 - filename must end with .eps and no further text is allowed"""
315 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
317 def baselevels(self, s, **kwargs):
318 return s
321 class _texmessageignore(_texmessageload):
322 """validates any TeX/LaTeX response
323 - this might be used, when the expression is ok, but no suitable texmessage
324 parser is available
325 - PLEASE: - consider writing suitable tex message parsers
326 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
328 __implements__ = _Itexmessage
330 def check(self, texrunner):
331 texrunner.texmessageparsed = ""
334 texmessage.start = _texmessagestart()
335 texmessage.noaux = _texmessagenofile("aux")
336 texmessage.nonav = _texmessagenofile("nav")
337 texmessage.end = _texmessageend()
338 texmessage.load = _texmessageload()
339 texmessage.loaddef = _texmessageloaddef()
340 texmessage.graphicsload = _texmessagegraphicsload()
341 texmessage.ignore = _texmessageignore()
343 # for internal use:
344 texmessage.inputmarker = _texmessageinputmarker()
345 texmessage.pyxbox = _texmessagepyxbox()
346 texmessage.pyxpageout = _texmessagepyxpageout()
347 texmessage.emptylines = _texmessageemptylines()
350 class _texmessageallwarning(texmessage):
351 """validates a given pattern 'pattern' as a warning 'warning'"""
353 def check(self, texrunner):
354 if texrunner.texmessageparsed:
355 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
356 texrunner.texmessageparsed = ""
358 texmessage.allwarning = _texmessageallwarning()
361 class texmessagepattern(texmessage):
362 """validates a given pattern and issue a warning (when set)"""
364 def __init__(self, pattern, warning=None):
365 self.pattern = pattern
366 self.warning = warning
368 def check(self, texrunner):
369 m = self.pattern.search(texrunner.texmessageparsed)
370 while m:
371 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
372 if self.warning:
373 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
374 m = self.pattern.search(texrunner.texmessageparsed)
376 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "ignoring font warning")
377 texmessage.boxwarning = texmessagepattern(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "ignoring overfull/underfull box warning")
378 texmessage.rerunwarning = texmessagepattern(re.compile(r"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re.MULTILINE), "ignoring rerun warning")
382 ###############################################################################
383 # textattrs
384 ###############################################################################
386 _textattrspreamble = ""
388 class textattr:
389 "a textattr defines a apply method, which modifies a (La)TeX expression"
391 class _localattr: pass
393 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
394 \def\PyXragged{%
395 \leftskip=0pt plus \PyXFlushHAlign fil%
396 \rightskip=0pt plus 1fil%
397 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
398 \parfillskip=0pt%
399 \pretolerance=9999%
400 \tolerance=9999%
401 \parindent=0pt%
402 \hyphenpenalty=9999%
403 \exhyphenpenalty=9999}%
406 class boxhalign(attr.exclusiveattr, textattr, _localattr):
408 def __init__(self, aboxhalign):
409 self.boxhalign = aboxhalign
410 attr.exclusiveattr.__init__(self, boxhalign)
412 def apply(self, expr):
413 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
415 boxhalign.left = boxhalign(0)
416 boxhalign.center = boxhalign(0.5)
417 boxhalign.right = boxhalign(1)
418 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
421 class flushhalign(attr.exclusiveattr, textattr, _localattr):
423 def __init__(self, aflushhalign):
424 self.flushhalign = aflushhalign
425 attr.exclusiveattr.__init__(self, flushhalign)
427 def apply(self, expr):
428 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
430 flushhalign.left = flushhalign(0)
431 flushhalign.center = flushhalign(0.5)
432 flushhalign.right = flushhalign(1)
433 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
436 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
438 def __init__(self, aboxhalign, aflushhalign):
439 self.boxhalign = aboxhalign
440 self.flushhalign = aflushhalign
441 attr.exclusiveattr.__init__(self, halign)
443 def apply(self, expr):
444 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
446 halign.left = halign(0, 0)
447 halign.center = halign(0.5, 0.5)
448 halign.right = halign(1, 1)
449 halign.clear = attr.clearclass(halign)
450 halign.boxleft = boxhalign.left
451 halign.boxcenter = boxhalign.center
452 halign.boxright = boxhalign.right
453 halign.flushleft = halign.raggedright = flushhalign.left
454 halign.flushcenter = halign.raggedcenter = flushhalign.center
455 halign.flushright = halign.raggedleft = flushhalign.right
458 class _mathmode(attr.attr, textattr, _localattr):
459 "math mode"
461 def apply(self, expr):
462 return r"$\displaystyle{%s}$" % expr
464 mathmode = _mathmode()
465 clearmathmode = attr.clearclass(_mathmode)
468 class _phantom(attr.attr, textattr, _localattr):
469 "phantom text"
471 def apply(self, expr):
472 return r"\phantom{%s}" % expr
474 phantom = _phantom()
475 clearphantom = attr.clearclass(_phantom)
478 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
480 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
482 top = 1
483 middle = 2
484 bottom = 3
486 def __init__(self, width, baseline=top):
487 self.width = width * 72.27 / (unit.scale["x"] * 72)
488 self.baseline = baseline
489 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
491 def apply(self, expr):
492 if self.baseline == self.top:
493 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
494 elif self.baseline == self.middle:
495 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)
496 elif self.baseline == self.bottom:
497 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
498 else:
499 RuntimeError("invalid baseline argument")
501 parbox_pt.clear = attr.clearclass(parbox_pt)
503 class parbox(parbox_pt):
505 def __init__(self, width, **kwargs):
506 parbox_pt.__init__(self, unit.topt(width), **kwargs)
508 parbox.clear = parbox_pt.clear
511 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
513 class valign(attr.sortbeforeexclusiveattr, textattr):
515 def __init__(self, avalign):
516 self.valign = avalign
517 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
519 def apply(self, expr):
520 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)
522 valign.top = valign(0)
523 valign.middle = valign(0.5)
524 valign.bottom = valign(1)
525 valign.clear = valign.baseline = attr.clearclass(valign)
528 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
530 class _vshift(attr.sortbeforeattr, textattr):
532 def __init__(self):
533 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
535 def apply(self, expr):
536 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
538 class vshift(_vshift):
539 "vertical down shift by a fraction of a character height"
541 def __init__(self, lowerratio, heightstr="0"):
542 _vshift.__init__(self)
543 self.lowerratio = lowerratio
544 self.heightstr = heightstr
546 def setheightexpr(self):
547 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
549 class _vshiftmathaxis(_vshift):
550 "vertical down shift by the height of the math axis"
552 def setheightexpr(self):
553 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
556 vshift.bottomzero = vshift(0)
557 vshift.middlezero = vshift(0.5)
558 vshift.topzero = vshift(1)
559 vshift.mathaxis = _vshiftmathaxis()
560 vshift.clear = attr.clearclass(_vshift)
563 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
564 None, "tiny", "scriptsize", "footnotesize", "small"]
566 class size(attr.sortbeforeattr, textattr):
567 "font size"
569 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
570 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
571 raise RuntimeError("either specify sizeindex or sizename")
572 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
573 if sizeindex is not None:
574 if sizeindex >= 0 and sizeindex < sizelist.index(None):
575 self.size = sizelist[sizeindex]
576 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
577 self.size = sizelist[sizeindex]
578 else:
579 raise IndexError("index out of sizelist range")
580 else:
581 self.size = sizename
583 def apply(self, expr):
584 return r"\%s{}%s" % (self.size, expr)
586 size.tiny = size(-4)
587 size.scriptsize = size.script = size(-3)
588 size.footnotesize = size.footnote = size(-2)
589 size.small = size(-1)
590 size.normalsize = size.normal = size(0)
591 size.large = size(1)
592 size.Large = size(2)
593 size.LARGE = size(3)
594 size.huge = size(4)
595 size.Huge = size(5)
596 size.clear = attr.clearclass(size)
599 ###############################################################################
600 # texrunner
601 ###############################################################################
604 class _readpipe(threading.Thread):
605 """threaded reader of TeX/LaTeX output
606 - sets an event, when a specific string in the programs output is found
607 - sets an event, when the terminal ends"""
609 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
610 """initialize the reader
611 - pipe: file to be read from
612 - expectqueue: keeps the next InputMarker to be wait for
613 - gotevent: the "got InputMarker" event
614 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
615 - quitevent: the "end of terminal" event"""
616 threading.Thread.__init__(self)
617 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
618 self.pipe = pipe
619 self.expectqueue = expectqueue
620 self.gotevent = gotevent
621 self.gotqueue = gotqueue
622 self.quitevent = quitevent
623 self.expect = None
624 self.start()
626 def run(self):
627 """thread routine"""
628 read = self.pipe.readline() # read, what comes in
629 try:
630 self.expect = self.expectqueue.get_nowait() # read, what should be expected
631 except Queue.Empty:
632 pass
633 while len(read):
634 # universal EOL handling (convert everything into unix like EOLs)
635 # XXX is this necessary on pipes?
636 read = read.replace("\r", "").replace("\n", "") + "\n"
637 self.gotqueue.put(read) # report, whats read
638 if self.expect is not None and read.find(self.expect) != -1:
639 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
640 read = self.pipe.readline() # read again
641 try:
642 self.expect = self.expectqueue.get_nowait()
643 except Queue.Empty:
644 pass
645 # EOF reached
646 self.pipe.close()
647 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
648 raise RuntimeError("TeX/LaTeX finished unexpectedly")
649 self.quitevent.set()
652 class textbox(box.rect, canvas._canvas):
653 """basically a box.rect, but it contains a text created by the texrunner
654 - texrunner._text and texrunner.text return such an object
655 - _textbox instances can be inserted into a canvas
656 - the output is contained in a page of the dvifile available thru the texrunner"""
657 # TODO: shouldn't all boxes become canvases? how about inserts then?
659 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
661 - finishdvi is a method to be called to get the dvicanvas
662 (e.g. the finishdvi calls the setdvicanvas method)
663 - attrs are fillstyles"""
664 self.left = left
665 self.right = right
666 self.width = left + right
667 self.height = height
668 self.depth = depth
669 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
670 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
671 canvas._canvas.__init__(self, attrs)
672 self.finishdvi = finishdvi
673 self.dvicanvas = None
674 self.insertdvicanvas = 0
676 def transform(self, *trafos):
677 if self.insertdvicanvas:
678 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
679 box.rect.transform(self, *trafos)
680 for trafo in trafos:
681 self.texttrafo = trafo * self.texttrafo
683 def setdvicanvas(self, dvicanvas):
684 if self.dvicanvas is not None:
685 raise RuntimeError("multiple call to setdvicanvas")
686 self.dvicanvas = dvicanvas
688 def ensuredvicanvas(self):
689 if self.dvicanvas is None:
690 self.finishdvi()
691 assert self.dvicanvas is not None, "finishdvi is broken"
692 if not self.insertdvicanvas:
693 self.insert(self.dvicanvas, [self.texttrafo])
694 self.insertdvicanvas = 1
696 def marker(self, marker):
697 self.ensuredvicanvas()
698 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
700 def processPS(self, file, writer, context, registry, bbox):
701 self.ensuredvicanvas()
702 abbox = bboxmodule.empty()
703 canvas._canvas.processPS(self, file, writer, context, registry, abbox)
704 bbox += box.rect.bbox(self)
706 def processPDF(self, file, writer, context, registry, bbox):
707 self.ensuredvicanvas()
708 abbox = bboxmodule.empty()
709 canvas._canvas.processPDF(self, file, writer, context, registry, abbox)
710 bbox += box.rect.bbox(self)
713 def _cleantmp(texrunner):
714 """get rid of temporary files
715 - function to be registered by atexit
716 - files contained in usefiles are kept"""
717 if texrunner.texruns: # cleanup while TeX is still running?
718 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
719 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
720 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
721 else:
722 texrunner.texinput.write("\n\\end\n")
723 texrunner.texinput.close() # close the input queue and
724 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
725 return # didn't got a quit from TeX -> we can't do much more
726 texrunner.texruns = 0
727 texrunner.texdone = 1
728 for usefile in texrunner.usefiles:
729 extpos = usefile.rfind(".")
730 try:
731 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
732 except OSError:
733 pass
734 for file in glob.glob("%s.*" % texrunner.texfilename):
735 try:
736 os.unlink(file)
737 except OSError:
738 pass
739 if texrunner.texdebug is not None:
740 try:
741 texrunner.texdebug.close()
742 texrunner.texdebug = None
743 except IOError:
744 pass
747 class _unset:
748 pass
750 class texrunner:
751 """TeX/LaTeX interface
752 - runs TeX/LaTeX expressions instantly
753 - checks TeX/LaTeX response
754 - the instance variable texmessage stores the last TeX
755 response as a string
756 - the instance variable texmessageparsed stores a parsed
757 version of texmessage; it should be empty after
758 texmessage.check was called, otherwise a TexResultError
759 is raised
760 - the instance variable errordebug controls the verbose
761 level of TexResultError"""
763 defaulttexmessagesstart = [texmessage.start]
764 defaulttexmessagesdocclass = [texmessage.load]
765 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
766 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning, texmessage.rerunwarning]
767 defaulttexmessagesdefaultpreamble = [texmessage.load]
768 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
769 texmessage.fontwarning, texmessage.boxwarning]
771 def __init__(self, mode="tex",
772 lfs="10pt",
773 docclass="article",
774 docopt=None,
775 usefiles=[],
776 waitfortex=config.getint("text", "waitfortex", 60),
777 showwaitfortex=config.getint("text", "showwaitfortex", 5),
778 texipc=config.getboolean("text", "texipc", 0),
779 texdebug=None,
780 dvidebug=0,
781 errordebug=1,
782 pyxgraphics=1,
783 texmessagesstart=[],
784 texmessagesdocclass=[],
785 texmessagesbegindoc=[],
786 texmessagesend=[],
787 texmessagesdefaultpreamble=[],
788 texmessagesdefaultrun=[]):
789 mode = mode.lower()
790 if mode != "tex" and mode != "latex":
791 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
792 self.mode = mode
793 self.lfs = lfs
794 self.docclass = docclass
795 self.docopt = docopt
796 self.usefiles = usefiles[:]
797 self.waitfortex = waitfortex
798 self.showwaitfortex = showwaitfortex
799 self.texipc = texipc
800 if texdebug is not None:
801 if texdebug[-4:] == ".tex":
802 self.texdebug = open(texdebug, "w")
803 else:
804 self.texdebug = open("%s.tex" % texdebug, "w")
805 else:
806 self.texdebug = None
807 self.dvidebug = dvidebug
808 self.errordebug = errordebug
809 self.pyxgraphics = pyxgraphics
810 self.texmessagesstart = texmessagesstart[:]
811 self.texmessagesdocclass = texmessagesdocclass[:]
812 self.texmessagesbegindoc = texmessagesbegindoc[:]
813 self.texmessagesend = texmessagesend[:]
814 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble[:]
815 self.texmessagesdefaultrun = texmessagesdefaultrun[:]
817 self.texruns = 0
818 self.texdone = 0
819 self.preamblemode = 1
820 self.executeid = 0
821 self.page = 0
822 self.preambles = []
823 self.needdvitextboxes = [] # when texipc-mode off
824 self.dvifile = None
825 self.textboxesincluded = 0
826 savetempdir = tempfile.tempdir
827 tempfile.tempdir = os.curdir
828 self.texfilename = os.path.basename(tempfile.mktemp())
829 tempfile.tempdir = savetempdir
831 def waitforevent(self, event):
832 """waits verbosely with an timeout for an event
833 - observes an event while periodly while printing messages
834 - returns the status of the event (isSet)
835 - does not clear the event"""
836 if self.showwaitfortex:
837 waited = 0
838 hasevent = 0
839 while waited < self.waitfortex and not hasevent:
840 if self.waitfortex - waited > self.showwaitfortex:
841 event.wait(self.showwaitfortex)
842 waited += self.showwaitfortex
843 else:
844 event.wait(self.waitfortex - waited)
845 waited += self.waitfortex - waited
846 hasevent = event.isSet()
847 if not hasevent:
848 if waited < self.waitfortex:
849 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
850 else:
851 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
852 return hasevent
853 else:
854 event.wait(self.waitfortex)
855 return event.isSet()
857 def execute(self, expr, texmessages):
858 """executes expr within TeX/LaTeX
859 - if self.texruns is not yet set, TeX/LaTeX is initialized,
860 self.texruns is set and self.preamblemode is set
861 - the method must not be called, when self.texdone is already set
862 - expr should be a string or None
863 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
864 self.texdone becomes set
865 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
866 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
867 - texmessages is a list of texmessage instances"""
868 if not self.texruns:
869 if self.texdebug is not None:
870 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
871 self.texdebug.write("%% mode: %s\n" % self.mode)
872 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
873 for usefile in self.usefiles:
874 extpos = usefile.rfind(".")
875 try:
876 os.rename(usefile, self.texfilename + usefile[extpos:])
877 except OSError:
878 pass
879 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
880 texfile.write("\\relax%\n")
881 texfile.close()
882 if self.texipc:
883 ipcflag = " --ipc"
884 else:
885 ipcflag = ""
886 try:
887 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
888 except ValueError:
889 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
890 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
891 atexit.register(_cleantmp, self)
892 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
893 self.gotevent = threading.Event() # keeps the got inputmarker event
894 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
895 self.quitevent = threading.Event() # keeps for end of terminal event
896 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
897 self.texruns = 1
898 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, 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 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 waitfortex is not _unset:
1113 self.waitfortex = waitfortex
1114 if showwaitfortex is not _unset:
1115 self.showwaitfortex = showwaitfortex
1116 if texipc is not _unset:
1117 self.texipc = texipc
1118 if texdebug is not _unset:
1119 if self.texdebug is not None:
1120 self.texdebug.close()
1121 if texdebug[-4:] == ".tex":
1122 self.texdebug = open(texdebug, "w")
1123 else:
1124 self.texdebug = open("%s.tex" % texdebug, "w")
1125 if dvidebug is not _unset:
1126 self.dvidebug = dvidebug
1127 if errordebug is not _unset:
1128 self.errordebug = errordebug
1129 if pyxgraphics is not _unset:
1130 self.pyxgraphics = pyxgraphics
1131 if errordebug is not _unset:
1132 self.errordebug = errordebug
1133 if texmessagesstart is not _unset:
1134 self.texmessagesstart = texmessagesstart
1135 if texmessagesdocclass is not _unset:
1136 self.texmessagesdocclass = texmessagesdocclass
1137 if texmessagesbegindoc is not _unset:
1138 self.texmessagesbegindoc = texmessagesbegindoc
1139 if texmessagesend is not _unset:
1140 self.texmessagesend = texmessagesend
1141 if texmessagesdefaultpreamble is not _unset:
1142 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1143 if texmessagesdefaultrun is not _unset:
1144 self.texmessagesdefaultrun = texmessagesdefaultrun
1146 def preamble(self, expr, texmessages=[]):
1147 r"""put something into the TeX/LaTeX preamble
1148 - in LaTeX, this is done before the \begin{document}
1149 (you might use \AtBeginDocument, when you're in need for)
1150 - it is not allowed to call preamble after calling the
1151 text method for the first time (for LaTeX this is needed
1152 due to \begin{document}; in TeX it is forced for compatibility
1153 (you should be able to switch from TeX to LaTeX, if you want,
1154 without breaking something)
1155 - preamble expressions must not create any dvi output
1156 - args might contain texmessage instances"""
1157 if self.texdone or not self.preamblemode:
1158 raise RuntimeError("preamble calls disabled due to previous text calls")
1159 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1160 self.execute(expr, texmessages)
1161 self.preambles.append((expr, texmessages))
1163 PyXBoxPattern = re.compile(r"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
1165 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1166 """create text by passing expr to TeX/LaTeX
1167 - returns a textbox containing the result from running expr thru TeX/LaTeX
1168 - the box center is set to x, y
1169 - *args may contain attr parameters, namely:
1170 - textattr instances
1171 - texmessage instances
1172 - trafo._trafo instances
1173 - style.fillstyle instances"""
1174 if expr is None:
1175 raise ValueError("None expression is invalid")
1176 if self.texdone:
1177 self.reset(reinit=1)
1178 first = 0
1179 if self.preamblemode:
1180 if self.mode == "latex":
1181 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1182 self.preamblemode = 0
1183 first = 1
1184 textattrs = attr.mergeattrs(textattrs) # perform cleans
1185 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1186 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1187 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1188 textattrs = attr.getattrs(textattrs, [textattr])
1189 # reverse loop over the merged textattrs (last is applied first)
1190 lentextattrs = len(textattrs)
1191 for i in range(lentextattrs):
1192 expr = textattrs[lentextattrs-1-i].apply(expr)
1193 try:
1194 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1195 except TexResultError:
1196 self.finishdvi(ignoretail=1)
1197 raise
1198 if self.texipc:
1199 if first:
1200 self.dvifile = dvifile.DVIfile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1201 match = self.PyXBoxPattern.search(self.texmessage)
1202 if not match or int(match.group("page")) != self.page:
1203 raise TexResultError("box extents not found", self)
1204 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1205 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1206 for t in trafos:
1207 box.reltransform(t) # TODO: should trafos really use reltransform???
1208 # this is quite different from what we do elsewhere!!!
1209 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1210 if self.texipc:
1211 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1212 else:
1213 self.needdvitextboxes.append(box)
1214 return box
1216 def text_pt(self, x, y, expr, *args, **kwargs):
1217 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1219 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1221 def textboxes(self, text, pageshapes):
1222 # this is some experimental code to put text into several boxes
1223 # while the bounding shape changes from box to box (rectangles only)
1224 # first we load sev.tex
1225 if not self.textboxesincluded:
1226 self.execute(r"\input textboxes.tex", [texmessage.load])
1227 self.textboxesincluded = 1
1228 # define page shapes
1229 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1230 pageshapes_str += "\\lohsizes={%\n"
1231 for hsize, vsize in pageshapes[1:]:
1232 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1233 pageshapes_str += "{\\relax}%\n}%\n"
1234 pageshapes_str += "\\lovsizes={%\n"
1235 for hsize, vsize in pageshapes[1:]:
1236 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1237 pageshapes_str += "{\\relax}%\n}%\n"
1238 page = 0
1239 parnos = []
1240 parshapes = []
1241 loop = 0
1242 while 1:
1243 self.execute(pageshapes_str, [])
1244 parnos_str = "}{".join(parnos)
1245 if parnos_str:
1246 parnos_str = "{%s}" % parnos_str
1247 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1248 self.execute(parnos_str, [])
1249 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1250 self.execute(parshapes_str, [])
1251 self.execute("\\global\\count0=1%%\n"
1252 "\\global\\parno=0%%\n"
1253 "\\global\\myprevgraf=0%%\n"
1254 "\\global\\showprevgraf=0%%\n"
1255 "\\global\\outputtype=0%%\n"
1256 "\\global\\leastcost=10000000%%\n"
1257 "%s%%\n"
1258 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1259 if self.texipc:
1260 if self.dvifile is None:
1261 self.dvifile = dvifile.DVIfile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1262 else:
1263 raise RuntimeError("textboxes currently needs texipc")
1264 lastparnos = parnos
1265 parnos = []
1266 lastparshapes = parshapes
1267 parshapes = []
1268 pages = 0
1269 lastpar = prevgraf = -1
1270 m = self.PyXVariableBoxPattern.search(self.texmessage)
1271 while m:
1272 pages += 1
1273 page = int(m.group("page"))
1274 assert page == pages
1275 par = int(m.group("par"))
1276 prevgraf = int(m.group("prevgraf"))
1277 if page <= len(pageshapes):
1278 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1279 else:
1280 width = 72.27/72*unit.topt(pageshapes[-1][0])
1281 if page < len(pageshapes):
1282 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1283 else:
1284 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1286 if par != lastpar:
1287 # a new paragraph is to be broken
1288 parnos.append(str(par))
1289 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1290 if len(parshape):
1291 parshape = " 0pt " + parshape
1292 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1293 elif prevgraf == lastprevgraf:
1294 pass
1295 else:
1296 # we have to append the breaking of the previous paragraph
1297 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1298 oldparshape = oldparshape.split('}')[0]
1299 if len(parshape):
1300 oldparshape = " " + oldparshape
1301 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1302 if len(parshape):
1303 parshape = " 0pt " + parshape
1304 else:
1305 parshape = " "
1306 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1307 lastpar = par
1308 lastprevgraf = prevgraf
1309 nextpos = m.end()
1310 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1311 result = []
1312 for i in range(pages):
1313 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1314 if parnos == lastparnos and parshapes == lastparshapes:
1315 return result
1316 loop += 1
1317 if loop > 100:
1318 raise TexResultError("Too many loops in textboxes ", texrunner)
1321 # the module provides an default texrunner and methods for direct access
1322 defaulttexrunner = texrunner()
1323 reset = defaulttexrunner.reset
1324 set = defaulttexrunner.set
1325 preamble = defaulttexrunner.preamble
1326 text = defaulttexrunner.text
1327 text_pt = defaulttexrunner.text_pt
1329 def escapestring(s, replace={" ": "~",
1330 "$": "\\$",
1331 "&": "\\&",
1332 "#": "\\#",
1333 "_": "\\_",
1334 "%": "\\%",
1335 "^": "\\string^",
1336 "~": "\\string~",
1337 "<": "{$<$}",
1338 ">": "{$>$}",
1339 "{": "{$\{$}",
1340 "}": "{$\}$}",
1341 "\\": "{$\setminus$}",
1342 "|": "{$\mid$}"}):
1343 "escape all ascii characters such that they are printable by TeX/LaTeX"
1344 i = 0
1345 while i < len(s):
1346 if not 32 <= ord(s[i]) < 127:
1347 raise ValueError("escapestring function handles ascii strings only")
1348 c = s[i]
1349 try:
1350 r = replace[c]
1351 except KeyError:
1352 i += 1
1353 else:
1354 s = s[:i] + r + s[i+1:]
1355 i += len(r)
1356 return s