- split dvifile into several files organized in new dvi directory
[PyX.git] / pyx / text.py
blob7abb1b3f0616d0e5cec3c6d9c813523ecda5f7f6
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 fontmaps=config.get("text", "fontmaps", "psfonts.map"),
777 waitfortex=config.getint("text", "waitfortex", 60),
778 showwaitfortex=config.getint("text", "showwaitfortex", 5),
779 texipc=config.getboolean("text", "texipc", 0),
780 texdebug=None,
781 dvidebug=0,
782 errordebug=1,
783 pyxgraphics=1,
784 texmessagesstart=[],
785 texmessagesdocclass=[],
786 texmessagesbegindoc=[],
787 texmessagesend=[],
788 texmessagesdefaultpreamble=[],
789 texmessagesdefaultrun=[]):
790 mode = mode.lower()
791 if mode != "tex" and mode != "latex":
792 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
793 self.mode = mode
794 self.lfs = lfs
795 self.docclass = docclass
796 self.docopt = docopt
797 self.usefiles = usefiles[:]
798 self.fontmaps = fontmaps
799 self.waitfortex = waitfortex
800 self.showwaitfortex = showwaitfortex
801 self.texipc = texipc
802 if texdebug is not None:
803 if texdebug[-4:] == ".tex":
804 self.texdebug = open(texdebug, "w")
805 else:
806 self.texdebug = open("%s.tex" % texdebug, "w")
807 else:
808 self.texdebug = None
809 self.dvidebug = dvidebug
810 self.errordebug = errordebug
811 self.pyxgraphics = pyxgraphics
812 self.texmessagesstart = texmessagesstart[:]
813 self.texmessagesdocclass = texmessagesdocclass[:]
814 self.texmessagesbegindoc = texmessagesbegindoc[:]
815 self.texmessagesend = texmessagesend[:]
816 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble[:]
817 self.texmessagesdefaultrun = texmessagesdefaultrun[:]
819 self.texruns = 0
820 self.texdone = 0
821 self.preamblemode = 1
822 self.executeid = 0
823 self.page = 0
824 self.preambles = []
825 self.needdvitextboxes = [] # when texipc-mode off
826 self.dvifile = None
827 self.textboxesincluded = 0
828 savetempdir = tempfile.tempdir
829 tempfile.tempdir = os.curdir
830 self.texfilename = os.path.basename(tempfile.mktemp())
831 tempfile.tempdir = savetempdir
833 def waitforevent(self, event):
834 """waits verbosely with an timeout for an event
835 - observes an event while periodly while printing messages
836 - returns the status of the event (isSet)
837 - does not clear the event"""
838 if self.showwaitfortex:
839 waited = 0
840 hasevent = 0
841 while waited < self.waitfortex and not hasevent:
842 if self.waitfortex - waited > self.showwaitfortex:
843 event.wait(self.showwaitfortex)
844 waited += self.showwaitfortex
845 else:
846 event.wait(self.waitfortex - waited)
847 waited += self.waitfortex - waited
848 hasevent = event.isSet()
849 if not hasevent:
850 if waited < self.waitfortex:
851 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
852 else:
853 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
854 return hasevent
855 else:
856 event.wait(self.waitfortex)
857 return event.isSet()
859 def execute(self, expr, texmessages):
860 """executes expr within TeX/LaTeX
861 - if self.texruns is not yet set, TeX/LaTeX is initialized,
862 self.texruns is set and self.preamblemode is set
863 - the method must not be called, when self.texdone is already set
864 - expr should be a string or None
865 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
866 self.texdone becomes set
867 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
868 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
869 - texmessages is a list of texmessage instances"""
870 if not self.texruns:
871 if self.texdebug is not None:
872 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
873 self.texdebug.write("%% mode: %s\n" % self.mode)
874 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
875 for usefile in self.usefiles:
876 extpos = usefile.rfind(".")
877 try:
878 os.rename(usefile, self.texfilename + usefile[extpos:])
879 except OSError:
880 pass
881 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
882 texfile.write("\\relax%\n")
883 texfile.close()
884 if self.texipc:
885 ipcflag = " --ipc"
886 else:
887 ipcflag = ""
888 try:
889 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
890 except ValueError:
891 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
892 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
893 atexit.register(_cleantmp, self)
894 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
895 self.gotevent = threading.Event() # keeps the got inputmarker event
896 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
897 self.quitevent = threading.Event() # keeps for end of terminal event
898 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
899 self.texruns = 1
900 self.fontmap = dvifile.readfontmap(self.fontmaps.split())
901 oldpreamblemode = self.preamblemode
902 self.preamblemode = 1
903 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
904 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
905 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
906 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
907 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
908 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
909 "\\newdimen\\PyXDimenHAlignRT%\n" +
910 _textattrspreamble + # insert preambles for textattrs macros
911 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
912 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
913 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
914 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
915 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
916 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
917 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
918 "lt=\\the\\PyXDimenHAlignLT,"
919 "rt=\\the\\PyXDimenHAlignRT,"
920 "ht=\\the\\ht\\PyXBox,"
921 "dp=\\the\\dp\\PyXBox:}%\n"
922 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
923 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
924 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
925 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
926 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
927 self.defaulttexmessagesstart + self.texmessagesstart)
928 os.remove("%s.tex" % self.texfilename)
929 if self.mode == "tex":
930 if self.lfs:
931 lfserror = None
932 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
933 lfsname = self.lfs
934 else:
935 lfsname = "%s.lfs" % self.lfs
936 for fulllfsname in [lfsname,
937 os.path.join(siteconfig.lfsdir, lfsname)]:
938 try:
939 lfsfile = open(fulllfsname, "r")
940 lfsdef = lfsfile.read()
941 lfsfile.close()
942 break
943 except IOError:
944 pass
945 else:
946 lfserror = "File '%s' is not available or not readable. " % lfsname
947 else:
948 lfserror = ""
949 if lfserror is not None:
950 allfiles = (glob.glob("*.lfs") +
951 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
952 lfsnames = []
953 for f in allfiles:
954 try:
955 open(f, "r").close()
956 lfsnames.append(os.path.basename(f)[:-4])
957 except IOError:
958 pass
959 lfsnames.sort()
960 if len(lfsnames):
961 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
962 else:
963 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
964 self.execute(lfsdef, [])
965 self.execute("\\normalsize%\n", [])
966 self.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
967 elif self.mode == "latex":
968 if self.pyxgraphics:
969 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
970 try:
971 open(pyxdef, "r").close()
972 except IOError:
973 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
974 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
975 self.execute("\\makeatletter%\n"
976 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
977 "\\def\\ProcessOptions{%\n"
978 "\\def\\Gin@driver{" + pyxdef + "}%\n"
979 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
980 "\\saveProcessOptions}%\n"
981 "\\makeatother",
983 if self.docopt is not None:
984 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
985 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
986 else:
987 self.execute("\\documentclass{%s}" % self.docclass,
988 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
989 self.preamblemode = oldpreamblemode
990 self.executeid += 1
991 if expr is not None: # TeX/LaTeX should process expr
992 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
993 if self.preamblemode:
994 self.expr = ("%s%%\n" % expr +
995 "\\PyXInput{%i}%%\n" % self.executeid)
996 else:
997 self.page += 1
998 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
999 "\\PyXInput{%i}%%\n" % self.executeid)
1000 else: # TeX/LaTeX should be finished
1001 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
1002 if self.mode == "latex":
1003 self.expr = "\\end{document}%\n"
1004 else:
1005 self.expr = "\\end%\n"
1006 if self.texdebug is not None:
1007 self.texdebug.write(self.expr)
1008 self.texinput.write(self.expr)
1009 gotevent = self.waitforevent(self.gotevent)
1010 self.gotevent.clear()
1011 if expr is None and gotevent: # TeX/LaTeX should have finished
1012 self.texruns = 0
1013 self.texdone = 1
1014 self.texinput.close() # close the input queue and
1015 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1016 try:
1017 self.texmessage = ""
1018 while 1:
1019 self.texmessage += self.gotqueue.get_nowait()
1020 except Queue.Empty:
1021 pass
1022 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1023 self.texmessageparsed = self.texmessage
1024 if gotevent:
1025 if expr is not None:
1026 texmessage.inputmarker.check(self)
1027 if not self.preamblemode:
1028 texmessage.pyxbox.check(self)
1029 texmessage.pyxpageout.check(self)
1030 texmessages = attr.mergeattrs(texmessages)
1031 for t in texmessages:
1032 t.check(self)
1033 keeptexmessageparsed = self.texmessageparsed
1034 texmessage.emptylines.check(self)
1035 if len(self.texmessageparsed):
1036 self.texmessageparsed = keeptexmessageparsed
1037 raise TexResultError("unhandled TeX response (might be an error)", self)
1038 else:
1039 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1041 def finishdvi(self, ignoretail=0):
1042 """finish TeX/LaTeX and read the dvifile
1043 - this method ensures that all textboxes can access their
1044 dvicanvas"""
1045 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1046 dvifilename = "%s.dvi" % self.texfilename
1047 if not self.texipc:
1048 self.dvifile = dvifile.dvifile(dvifilename, self.fontmap, debug=self.dvidebug)
1049 page = 1
1050 for box in self.needdvitextboxes:
1051 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0]))
1052 page += 1
1053 if not ignoretail and self.dvifile.readpage(None) is not None:
1054 raise RuntimeError("end of dvifile expected")
1055 self.dvifile = None
1056 self.needdvitextboxes = []
1058 def reset(self, reinit=0):
1059 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1060 if self.texruns:
1061 self.finishdvi()
1062 if self.texdebug is not None:
1063 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1064 self.executeid = 0
1065 self.page = 0
1066 self.texdone = 0
1067 if reinit:
1068 self.preamblemode = 1
1069 for expr, texmessages in self.preambles:
1070 self.execute(expr, texmessages)
1071 if self.mode == "latex":
1072 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1073 self.preamblemode = 0
1074 else:
1075 self.preambles = []
1076 self.preamblemode = 1
1078 def set(self, mode=_unset,
1079 lfs=_unset,
1080 docclass=_unset,
1081 docopt=_unset,
1082 usefiles=_unset,
1083 fontmaps=_unset,
1084 waitfortex=_unset,
1085 showwaitfortex=_unset,
1086 texipc=_unset,
1087 texdebug=_unset,
1088 dvidebug=_unset,
1089 errordebug=_unset,
1090 pyxgraphics=_unset,
1091 texmessagesstart=_unset,
1092 texmessagesdocclass=_unset,
1093 texmessagesbegindoc=_unset,
1094 texmessagesend=_unset,
1095 texmessagesdefaultpreamble=_unset,
1096 texmessagesdefaultrun=_unset):
1097 """provide a set command for TeX/LaTeX settings
1098 - TeX/LaTeX must not yet been started
1099 - especially needed for the defaultrunner, where no access to
1100 the constructor is available"""
1101 if self.texruns:
1102 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1103 if mode is not _unset:
1104 mode = mode.lower()
1105 if mode != "tex" and mode != "latex":
1106 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1107 self.mode = mode
1108 if lfs is not _unset:
1109 self.lfs = lfs
1110 if docclass is not _unset:
1111 self.docclass = docclass
1112 if docopt is not _unset:
1113 self.docopt = docopt
1114 if usefiles is not _unset:
1115 self.usefiles = usefiles
1116 if fontmaps is not _unset:
1117 self.fontmaps = fontmaps
1118 if waitfortex is not _unset:
1119 self.waitfortex = waitfortex
1120 if showwaitfortex is not _unset:
1121 self.showwaitfortex = showwaitfortex
1122 if texipc is not _unset:
1123 self.texipc = texipc
1124 if texdebug is not _unset:
1125 if self.texdebug is not None:
1126 self.texdebug.close()
1127 if texdebug[-4:] == ".tex":
1128 self.texdebug = open(texdebug, "w")
1129 else:
1130 self.texdebug = open("%s.tex" % texdebug, "w")
1131 if dvidebug is not _unset:
1132 self.dvidebug = dvidebug
1133 if errordebug is not _unset:
1134 self.errordebug = errordebug
1135 if pyxgraphics is not _unset:
1136 self.pyxgraphics = pyxgraphics
1137 if errordebug is not _unset:
1138 self.errordebug = errordebug
1139 if texmessagesstart is not _unset:
1140 self.texmessagesstart = texmessagesstart
1141 if texmessagesdocclass is not _unset:
1142 self.texmessagesdocclass = texmessagesdocclass
1143 if texmessagesbegindoc is not _unset:
1144 self.texmessagesbegindoc = texmessagesbegindoc
1145 if texmessagesend is not _unset:
1146 self.texmessagesend = texmessagesend
1147 if texmessagesdefaultpreamble is not _unset:
1148 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1149 if texmessagesdefaultrun is not _unset:
1150 self.texmessagesdefaultrun = texmessagesdefaultrun
1152 def preamble(self, expr, texmessages=[]):
1153 r"""put something into the TeX/LaTeX preamble
1154 - in LaTeX, this is done before the \begin{document}
1155 (you might use \AtBeginDocument, when you're in need for)
1156 - it is not allowed to call preamble after calling the
1157 text method for the first time (for LaTeX this is needed
1158 due to \begin{document}; in TeX it is forced for compatibility
1159 (you should be able to switch from TeX to LaTeX, if you want,
1160 without breaking something)
1161 - preamble expressions must not create any dvi output
1162 - args might contain texmessage instances"""
1163 if self.texdone or not self.preamblemode:
1164 raise RuntimeError("preamble calls disabled due to previous text calls")
1165 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1166 self.execute(expr, texmessages)
1167 self.preambles.append((expr, texmessages))
1169 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:")
1171 def text(self, x, y, expr, textattrs=[], texmessages=[]):
1172 """create text by passing expr to TeX/LaTeX
1173 - returns a textbox containing the result from running expr thru TeX/LaTeX
1174 - the box center is set to x, y
1175 - *args may contain attr parameters, namely:
1176 - textattr instances
1177 - texmessage instances
1178 - trafo._trafo instances
1179 - style.fillstyle instances"""
1180 if expr is None:
1181 raise ValueError("None expression is invalid")
1182 if self.texdone:
1183 self.reset(reinit=1)
1184 first = 0
1185 if self.preamblemode:
1186 if self.mode == "latex":
1187 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1188 self.preamblemode = 0
1189 first = 1
1190 textattrs = attr.mergeattrs(textattrs) # perform cleans
1191 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1192 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1193 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1194 textattrs = attr.getattrs(textattrs, [textattr])
1195 # reverse loop over the merged textattrs (last is applied first)
1196 lentextattrs = len(textattrs)
1197 for i in range(lentextattrs):
1198 expr = textattrs[lentextattrs-1-i].apply(expr)
1199 try:
1200 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1201 except TexResultError:
1202 self.finishdvi(ignoretail=1)
1203 raise
1204 if self.texipc:
1205 if first:
1206 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1207 match = self.PyXBoxPattern.search(self.texmessage)
1208 if not match or int(match.group("page")) != self.page:
1209 raise TexResultError("box extents not found", self)
1210 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1211 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1212 for t in trafos:
1213 box.reltransform(t) # TODO: should trafos really use reltransform???
1214 # this is quite different from what we do elsewhere!!!
1215 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1216 if self.texipc:
1217 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0]))
1218 else:
1219 self.needdvitextboxes.append(box)
1220 return box
1222 def text_pt(self, x, y, expr, *args, **kwargs):
1223 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1225 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1227 def textboxes(self, text, pageshapes):
1228 # this is some experimental code to put text into several boxes
1229 # while the bounding shape changes from box to box (rectangles only)
1230 # first we load sev.tex
1231 if not self.textboxesincluded:
1232 self.execute(r"\input textboxes.tex", [texmessage.load])
1233 self.textboxesincluded = 1
1234 # define page shapes
1235 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1236 pageshapes_str += "\\lohsizes={%\n"
1237 for hsize, vsize in pageshapes[1:]:
1238 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1239 pageshapes_str += "{\\relax}%\n}%\n"
1240 pageshapes_str += "\\lovsizes={%\n"
1241 for hsize, vsize in pageshapes[1:]:
1242 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1243 pageshapes_str += "{\\relax}%\n}%\n"
1244 page = 0
1245 parnos = []
1246 parshapes = []
1247 loop = 0
1248 while 1:
1249 self.execute(pageshapes_str, [])
1250 parnos_str = "}{".join(parnos)
1251 if parnos_str:
1252 parnos_str = "{%s}" % parnos_str
1253 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1254 self.execute(parnos_str, [])
1255 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1256 self.execute(parshapes_str, [])
1257 self.execute("\\global\\count0=1%%\n"
1258 "\\global\\parno=0%%\n"
1259 "\\global\\myprevgraf=0%%\n"
1260 "\\global\\showprevgraf=0%%\n"
1261 "\\global\\outputtype=0%%\n"
1262 "\\global\\leastcost=10000000%%\n"
1263 "%s%%\n"
1264 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1265 if self.texipc:
1266 if self.dvifile is None:
1267 self.dvifile = dvifile.dvifile("%s.dvi" % self.texfilename, self.fontmap, debug=self.dvidebug)
1268 else:
1269 raise RuntimeError("textboxes currently needs texipc")
1270 lastparnos = parnos
1271 parnos = []
1272 lastparshapes = parshapes
1273 parshapes = []
1274 pages = 0
1275 lastpar = prevgraf = -1
1276 m = self.PyXVariableBoxPattern.search(self.texmessage)
1277 while m:
1278 pages += 1
1279 page = int(m.group("page"))
1280 assert page == pages
1281 par = int(m.group("par"))
1282 prevgraf = int(m.group("prevgraf"))
1283 if page <= len(pageshapes):
1284 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1285 else:
1286 width = 72.27/72*unit.topt(pageshapes[-1][0])
1287 if page < len(pageshapes):
1288 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1289 else:
1290 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1292 if par != lastpar:
1293 # a new paragraph is to be broken
1294 parnos.append(str(par))
1295 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1296 if len(parshape):
1297 parshape = " 0pt " + parshape
1298 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1299 elif prevgraf == lastprevgraf:
1300 pass
1301 else:
1302 # we have to append the breaking of the previous paragraph
1303 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1304 oldparshape = oldparshape.split('}')[0]
1305 if len(parshape):
1306 oldparshape = " " + oldparshape
1307 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1308 if len(parshape):
1309 parshape = " 0pt " + parshape
1310 else:
1311 parshape = " "
1312 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1313 lastpar = par
1314 lastprevgraf = prevgraf
1315 nextpos = m.end()
1316 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1317 result = []
1318 for i in range(pages):
1319 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1320 if parnos == lastparnos and parshapes == lastparshapes:
1321 return result
1322 loop += 1
1323 if loop > 100:
1324 raise TexResultError("Too many loops in textboxes ", texrunner)
1327 # the module provides an default texrunner and methods for direct access
1328 defaulttexrunner = texrunner()
1329 reset = defaulttexrunner.reset
1330 set = defaulttexrunner.set
1331 preamble = defaulttexrunner.preamble
1332 text = defaulttexrunner.text
1333 text_pt = defaulttexrunner.text_pt
1335 def escapestring(s, replace={" ": "~",
1336 "$": "\\$",
1337 "&": "\\&",
1338 "#": "\\#",
1339 "_": "\\_",
1340 "%": "\\%",
1341 "^": "\\string^",
1342 "~": "\\string~",
1343 "<": "{$<$}",
1344 ">": "{$>$}",
1345 "{": "{$\{$}",
1346 "}": "{$\}$}",
1347 "\\": "{$\setminus$}",
1348 "|": "{$\mid$}"}):
1349 "escape all ascii characters such that they are printable by TeX/LaTeX"
1350 i = 0
1351 while i < len(s):
1352 if not 32 <= ord(s[i]) < 127:
1353 raise ValueError("escapestring function handles ascii strings only")
1354 c = s[i]
1355 try:
1356 r = replace[c]
1357 except KeyError:
1358 i += 1
1359 else:
1360 s = s[:i] + r + s[i+1:]
1361 i += len(r)
1362 return s