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