added a messageparser and deletion of files for revtex4-1
[PyX/mjg.git] / pyx / text.py
blob74194650d1bbc70a61d4befbe6356e155fdd0db7
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")
381 texmessage.nobblwarning = texmessagepattern(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE), "ignoring no-bbl warning")
385 ###############################################################################
386 # textattrs
387 ###############################################################################
389 _textattrspreamble = ""
391 class textattr:
392 "a textattr defines a apply method, which modifies a (La)TeX expression"
394 class _localattr: pass
396 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
397 \def\PyXragged{%
398 \leftskip=0pt plus \PyXFlushHAlign fil%
399 \rightskip=0pt plus 1fil%
400 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
401 \parfillskip=0pt%
402 \pretolerance=9999%
403 \tolerance=9999%
404 \parindent=0pt%
405 \hyphenpenalty=9999%
406 \exhyphenpenalty=9999}%
409 class boxhalign(attr.exclusiveattr, textattr, _localattr):
411 def __init__(self, aboxhalign):
412 self.boxhalign = aboxhalign
413 attr.exclusiveattr.__init__(self, boxhalign)
415 def apply(self, expr):
416 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
418 boxhalign.left = boxhalign(0)
419 boxhalign.center = boxhalign(0.5)
420 boxhalign.right = boxhalign(1)
421 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
424 class flushhalign(attr.exclusiveattr, textattr, _localattr):
426 def __init__(self, aflushhalign):
427 self.flushhalign = aflushhalign
428 attr.exclusiveattr.__init__(self, flushhalign)
430 def apply(self, expr):
431 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
433 flushhalign.left = flushhalign(0)
434 flushhalign.center = flushhalign(0.5)
435 flushhalign.right = flushhalign(1)
436 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
439 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
441 def __init__(self, aboxhalign, aflushhalign):
442 self.boxhalign = aboxhalign
443 self.flushhalign = aflushhalign
444 attr.exclusiveattr.__init__(self, halign)
446 def apply(self, expr):
447 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
449 halign.left = halign(0, 0)
450 halign.center = halign(0.5, 0.5)
451 halign.right = halign(1, 1)
452 halign.clear = attr.clearclass(halign)
453 halign.boxleft = boxhalign.left
454 halign.boxcenter = boxhalign.center
455 halign.boxright = boxhalign.right
456 halign.flushleft = halign.raggedright = flushhalign.left
457 halign.flushcenter = halign.raggedcenter = flushhalign.center
458 halign.flushright = halign.raggedleft = flushhalign.right
461 class _mathmode(attr.attr, textattr, _localattr):
462 "math mode"
464 def apply(self, expr):
465 return r"$\displaystyle{%s}$" % expr
467 mathmode = _mathmode()
468 clearmathmode = attr.clearclass(_mathmode)
471 class _phantom(attr.attr, textattr, _localattr):
472 "phantom text"
474 def apply(self, expr):
475 return r"\phantom{%s}" % expr
477 phantom = _phantom()
478 clearphantom = attr.clearclass(_phantom)
481 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
483 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
485 top = 1
486 middle = 2
487 bottom = 3
489 def __init__(self, width, baseline=top):
490 self.width = width * 72.27 / (unit.scale["x"] * 72)
491 self.baseline = baseline
492 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
494 def apply(self, expr):
495 if self.baseline == self.top:
496 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
497 elif self.baseline == self.middle:
498 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)
499 elif self.baseline == self.bottom:
500 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
501 else:
502 RuntimeError("invalid baseline argument")
504 parbox_pt.clear = attr.clearclass(parbox_pt)
506 class parbox(parbox_pt):
508 def __init__(self, width, **kwargs):
509 parbox_pt.__init__(self, unit.topt(width), **kwargs)
511 parbox.clear = parbox_pt.clear
514 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
516 class valign(attr.sortbeforeexclusiveattr, textattr):
518 def __init__(self, avalign):
519 self.valign = avalign
520 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
522 def apply(self, expr):
523 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)
525 valign.top = valign(0)
526 valign.middle = valign(0.5)
527 valign.bottom = valign(1)
528 valign.clear = valign.baseline = attr.clearclass(valign)
531 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
533 class _vshift(attr.sortbeforeattr, textattr):
535 def __init__(self):
536 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
538 def apply(self, expr):
539 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
541 class vshift(_vshift):
542 "vertical down shift by a fraction of a character height"
544 def __init__(self, lowerratio, heightstr="0"):
545 _vshift.__init__(self)
546 self.lowerratio = lowerratio
547 self.heightstr = heightstr
549 def setheightexpr(self):
550 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
552 class _vshiftmathaxis(_vshift):
553 "vertical down shift by the height of the math axis"
555 def setheightexpr(self):
556 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
559 vshift.bottomzero = vshift(0)
560 vshift.middlezero = vshift(0.5)
561 vshift.topzero = vshift(1)
562 vshift.mathaxis = _vshiftmathaxis()
563 vshift.clear = attr.clearclass(_vshift)
566 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
567 None, "tiny", "scriptsize", "footnotesize", "small"]
569 class size(attr.sortbeforeattr, textattr):
570 "font size"
572 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
573 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
574 raise RuntimeError("either specify sizeindex or sizename")
575 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
576 if sizeindex is not None:
577 if sizeindex >= 0 and sizeindex < sizelist.index(None):
578 self.size = sizelist[sizeindex]
579 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
580 self.size = sizelist[sizeindex]
581 else:
582 raise IndexError("index out of sizelist range")
583 else:
584 self.size = sizename
586 def apply(self, expr):
587 return r"\%s{}%s" % (self.size, expr)
589 size.tiny = size(-4)
590 size.scriptsize = size.script = size(-3)
591 size.footnotesize = size.footnote = size(-2)
592 size.small = size(-1)
593 size.normalsize = size.normal = size(0)
594 size.large = size(1)
595 size.Large = size(2)
596 size.LARGE = size(3)
597 size.huge = size(4)
598 size.Huge = size(5)
599 size.clear = attr.clearclass(size)
602 ###############################################################################
603 # texrunner
604 ###############################################################################
607 class _readpipe(threading.Thread):
608 """threaded reader of TeX/LaTeX output
609 - sets an event, when a specific string in the programs output is found
610 - sets an event, when the terminal ends"""
612 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
613 """initialize the reader
614 - pipe: file to be read from
615 - expectqueue: keeps the next InputMarker to be wait for
616 - gotevent: the "got InputMarker" event
617 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
618 - quitevent: the "end of terminal" event"""
619 threading.Thread.__init__(self)
620 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
621 self.pipe = pipe
622 self.expectqueue = expectqueue
623 self.gotevent = gotevent
624 self.gotqueue = gotqueue
625 self.quitevent = quitevent
626 self.expect = None
628 def run(self):
629 """thread routine"""
630 def _read():
631 # catch interupted system call errors while reading
632 while 1:
633 try:
634 return self.pipe.readline()
635 except IOError, e:
636 if e.errno != errno.EINTR:
637 raise
638 read = _read() # read, what comes in
639 try:
640 self.expect = self.expectqueue.get_nowait() # read, what should be expected
641 except Queue.Empty:
642 pass
643 while len(read):
644 # universal EOL handling (convert everything into unix like EOLs)
645 # XXX is this necessary on pipes?
646 read = read.replace("\r", "").replace("\n", "") + "\n"
647 self.gotqueue.put(read) # report, whats read
648 if self.expect is not None and read.find(self.expect) != -1:
649 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
650 read = _read() # read again
651 try:
652 self.expect = self.expectqueue.get_nowait()
653 except Queue.Empty:
654 pass
655 # EOF reached
656 self.pipe.close()
657 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
658 raise RuntimeError("TeX/LaTeX finished unexpectedly")
659 self.quitevent.set()
662 class textbox(box.rect, canvas._canvas):
663 """basically a box.rect, but it contains a text created by the texrunner
664 - texrunner._text and texrunner.text return such an object
665 - _textbox instances can be inserted into a canvas
666 - the output is contained in a page of the dvifile available thru the texrunner"""
667 # TODO: shouldn't all boxes become canvases? how about inserts then?
669 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
671 - finishdvi is a method to be called to get the dvicanvas
672 (e.g. the finishdvi calls the setdvicanvas method)
673 - attrs are fillstyles"""
674 self.left = left
675 self.right = right
676 self.width = left + right
677 self.height = height
678 self.depth = depth
679 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
680 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
681 canvas._canvas.__init__(self, attrs)
682 self.finishdvi = finishdvi
683 self.dvicanvas = None
684 self.insertdvicanvas = 0
686 def transform(self, *trafos):
687 if self.insertdvicanvas:
688 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
689 box.rect.transform(self, *trafos)
690 for trafo in trafos:
691 self.texttrafo = trafo * self.texttrafo
693 def setdvicanvas(self, dvicanvas):
694 if self.dvicanvas is not None:
695 raise RuntimeError("multiple call to setdvicanvas")
696 self.dvicanvas = dvicanvas
698 def ensuredvicanvas(self):
699 if self.dvicanvas is None:
700 self.finishdvi()
701 assert self.dvicanvas is not None, "finishdvi is broken"
702 if not self.insertdvicanvas:
703 self.insert(self.dvicanvas, [self.texttrafo])
704 self.insertdvicanvas = 1
706 def marker(self, marker):
707 self.ensuredvicanvas()
708 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
710 def processPS(self, file, writer, context, registry, bbox):
711 self.ensuredvicanvas()
712 abbox = bboxmodule.empty()
713 canvas._canvas.processPS(self, file, writer, context, registry, abbox)
714 bbox += box.rect.bbox(self)
716 def processPDF(self, file, writer, context, registry, bbox):
717 self.ensuredvicanvas()
718 abbox = bboxmodule.empty()
719 canvas._canvas.processPDF(self, file, writer, context, registry, abbox)
720 bbox += box.rect.bbox(self)
723 def _cleantmp(texrunner):
724 """get rid of temporary files
725 - function to be registered by atexit
726 - files contained in usefiles are kept"""
727 if texrunner.texruns: # cleanup while TeX is still running?
728 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
729 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
730 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
731 else:
732 texrunner.texinput.write("\n\\end\n")
733 texrunner.texinput.close() # close the input queue and
734 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
735 return # didn't got a quit from TeX -> we can't do much more
736 texrunner.texruns = 0
737 texrunner.texdone = 1
738 for usefile in texrunner.usefiles:
739 extpos = usefile.rfind(".")
740 try:
741 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
742 except OSError:
743 pass
744 for file in glob.glob("%s.*" % texrunner.texfilename) + ["%sNotes.bib" % texrunner.texfilename]:
745 try:
746 os.unlink(file)
747 except OSError:
748 pass
749 if texrunner.texdebug is not None:
750 try:
751 texrunner.texdebug.close()
752 texrunner.texdebug = None
753 except IOError:
754 pass
757 class _unset:
758 pass
760 class texrunner:
761 """TeX/LaTeX interface
762 - runs TeX/LaTeX expressions instantly
763 - checks TeX/LaTeX response
764 - the instance variable texmessage stores the last TeX
765 response as a string
766 - the instance variable texmessageparsed stores a parsed
767 version of texmessage; it should be empty after
768 texmessage.check was called, otherwise a TexResultError
769 is raised
770 - the instance variable errordebug controls the verbose
771 level of TexResultError"""
773 defaulttexmessagesstart = [texmessage.start]
774 defaulttexmessagesdocclass = [texmessage.load]
775 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
776 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning, texmessage.rerunwarning, texmessage.nobblwarning]
777 defaulttexmessagesdefaultpreamble = [texmessage.load]
778 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
779 texmessage.fontwarning, texmessage.boxwarning]
781 def __init__(self, mode="tex",
782 lfs="10pt",
783 docclass="article",
784 docopt=None,
785 usefiles=[],
786 waitfortex=config.getint("text", "waitfortex", 60),
787 showwaitfortex=config.getint("text", "showwaitfortex", 5),
788 texipc=config.getboolean("text", "texipc", 0),
789 texdebug=None,
790 dvidebug=0,
791 errordebug=1,
792 pyxgraphics=1,
793 texmessagesstart=[],
794 texmessagesdocclass=[],
795 texmessagesbegindoc=[],
796 texmessagesend=[],
797 texmessagesdefaultpreamble=[],
798 texmessagesdefaultrun=[]):
799 mode = mode.lower()
800 if mode != "tex" and mode != "latex":
801 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
802 self.mode = mode
803 self.lfs = lfs
804 self.docclass = docclass
805 self.docopt = docopt
806 self.usefiles = usefiles[:]
807 self.waitfortex = waitfortex
808 self.showwaitfortex = showwaitfortex
809 self.texipc = texipc
810 if texdebug is not None:
811 if texdebug[-4:] == ".tex":
812 self.texdebug = open(texdebug, "w")
813 else:
814 self.texdebug = open("%s.tex" % texdebug, "w")
815 else:
816 self.texdebug = None
817 self.dvidebug = dvidebug
818 self.errordebug = errordebug
819 self.pyxgraphics = pyxgraphics
820 self.texmessagesstart = texmessagesstart[:]
821 self.texmessagesdocclass = texmessagesdocclass[:]
822 self.texmessagesbegindoc = texmessagesbegindoc[:]
823 self.texmessagesend = texmessagesend[:]
824 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble[:]
825 self.texmessagesdefaultrun = texmessagesdefaultrun[:]
827 self.texruns = 0
828 self.texdone = 0
829 self.preamblemode = 1
830 self.executeid = 0
831 self.page = 0
832 self.preambles = []
833 self.needdvitextboxes = [] # when texipc-mode off
834 self.dvifile = None
835 self.textboxesincluded = 0
836 savetempdir = tempfile.tempdir
837 tempfile.tempdir = os.curdir
838 self.texfilename = os.path.basename(tempfile.mktemp())
839 tempfile.tempdir = savetempdir
841 def waitforevent(self, event):
842 """waits verbosely with an timeout for an event
843 - observes an event while periodly while printing messages
844 - returns the status of the event (isSet)
845 - does not clear the event"""
846 if self.showwaitfortex:
847 waited = 0
848 hasevent = 0
849 while waited < self.waitfortex and not hasevent:
850 if self.waitfortex - waited > self.showwaitfortex:
851 event.wait(self.showwaitfortex)
852 waited += self.showwaitfortex
853 else:
854 event.wait(self.waitfortex - waited)
855 waited += self.waitfortex - waited
856 hasevent = event.isSet()
857 if not hasevent:
858 if waited < self.waitfortex:
859 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex))
860 else:
861 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode))
862 return hasevent
863 else:
864 event.wait(self.waitfortex)
865 return event.isSet()
867 def execute(self, expr, texmessages):
868 """executes expr within TeX/LaTeX
869 - if self.texruns is not yet set, TeX/LaTeX is initialized,
870 self.texruns is set and self.preamblemode is set
871 - the method must not be called, when self.texdone is already set
872 - expr should be a string or None
873 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
874 self.texdone becomes set
875 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
876 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
877 - texmessages is a list of texmessage instances"""
878 if not self.texruns:
879 if self.texdebug is not None:
880 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
881 self.texdebug.write("%% mode: %s\n" % self.mode)
882 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
883 for usefile in self.usefiles:
884 extpos = usefile.rfind(".")
885 try:
886 os.rename(usefile, self.texfilename + usefile[extpos:])
887 except OSError:
888 pass
889 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
890 texfile.write("\\relax%\n")
891 texfile.close()
892 if self.texipc:
893 ipcflag = " --ipc"
894 else:
895 ipcflag = ""
896 try:
897 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
898 except ValueError:
899 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
900 self.texinput, self.texoutput = os.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
901 atexit.register(_cleantmp, self)
902 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
903 self.gotevent = threading.Event() # keeps the got inputmarker event
904 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
905 self.quitevent = threading.Event() # keeps for end of terminal event
906 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
907 self.texruns = 1
908 oldpreamblemode = self.preamblemode
909 self.preamblemode = 1
910 self.readoutput.start()
911 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
912 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
913 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
914 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
915 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
916 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
917 "\\newdimen\\PyXDimenHAlignRT%\n" +
918 _textattrspreamble + # insert preambles for textattrs macros
919 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
920 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
921 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
922 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
923 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
924 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
925 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
926 "lt=\\the\\PyXDimenHAlignLT,"
927 "rt=\\the\\PyXDimenHAlignRT,"
928 "ht=\\the\\ht\\PyXBox,"
929 "dp=\\the\\dp\\PyXBox:}%\n"
930 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
931 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
932 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
933 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
934 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
935 self.defaulttexmessagesstart + self.texmessagesstart)
936 os.remove("%s.tex" % self.texfilename)
937 if self.mode == "tex":
938 if self.lfs:
939 lfserror = None
940 if len(self.lfs) > 4 and self.lfs[-4:] == ".lfs":
941 lfsname = self.lfs
942 else:
943 lfsname = "%s.lfs" % self.lfs
944 for fulllfsname in [lfsname,
945 os.path.join(siteconfig.lfsdir, lfsname)]:
946 try:
947 lfsfile = open(fulllfsname, "r")
948 lfsdef = lfsfile.read()
949 lfsfile.close()
950 break
951 except IOError:
952 pass
953 else:
954 lfserror = "File '%s' is not available or not readable. " % lfsname
955 else:
956 lfserror = ""
957 if lfserror is not None:
958 allfiles = (glob.glob("*.lfs") +
959 glob.glob(os.path.join(siteconfig.lfsdir, "*.lfs")))
960 lfsnames = []
961 for f in allfiles:
962 try:
963 open(f, "r").close()
964 lfsnames.append(os.path.basename(f)[:-4])
965 except IOError:
966 pass
967 lfsnames.sort()
968 if len(lfsnames):
969 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror, lfsnames))
970 else:
971 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror)
972 self.execute(lfsdef, [])
973 self.execute("\\normalsize%\n", [])
974 self.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
975 elif self.mode == "latex":
976 if self.pyxgraphics:
977 pyxdef = os.path.join(siteconfig.sharedir, "pyx.def")
978 try:
979 open(pyxdef, "r").close()
980 except IOError:
981 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
982 pyxdef = os.path.abspath(pyxdef).replace(os.sep, "/")
983 self.execute("\\makeatletter%\n"
984 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
985 "\\def\\ProcessOptions{%\n"
986 "\\def\\Gin@driver{" + pyxdef + "}%\n"
987 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
988 "\\saveProcessOptions}%\n"
989 "\\makeatother",
991 if self.docopt is not None:
992 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
993 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
994 else:
995 self.execute("\\documentclass{%s}" % self.docclass,
996 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
997 self.preamblemode = oldpreamblemode
998 self.executeid += 1
999 if expr is not None: # TeX/LaTeX should process expr
1000 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
1001 if self.preamblemode:
1002 self.expr = ("%s%%\n" % expr +
1003 "\\PyXInput{%i}%%\n" % self.executeid)
1004 else:
1005 self.page += 1
1006 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
1007 "\\PyXInput{%i}%%\n" % self.executeid)
1008 else: # TeX/LaTeX should be finished
1009 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
1010 if self.mode == "latex":
1011 self.expr = "\\end{document}%\n"
1012 else:
1013 self.expr = "\\end%\n"
1014 if self.texdebug is not None:
1015 self.texdebug.write(self.expr)
1016 self.texinput.write(self.expr)
1017 gotevent = self.waitforevent(self.gotevent)
1018 self.gotevent.clear()
1019 if expr is None and gotevent: # TeX/LaTeX should have finished
1020 self.texruns = 0
1021 self.texdone = 1
1022 self.texinput.close() # close the input queue and
1023 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1024 try:
1025 self.texmessage = ""
1026 while 1:
1027 self.texmessage += self.gotqueue.get_nowait()
1028 except Queue.Empty:
1029 pass
1030 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1031 self.texmessageparsed = self.texmessage
1032 if gotevent:
1033 if expr is not None:
1034 texmessage.inputmarker.check(self)
1035 if not self.preamblemode:
1036 texmessage.pyxbox.check(self)
1037 texmessage.pyxpageout.check(self)
1038 texmessages = attr.mergeattrs(texmessages)
1039 for t in texmessages:
1040 t.check(self)
1041 keeptexmessageparsed = self.texmessageparsed
1042 texmessage.emptylines.check(self)
1043 if len(self.texmessageparsed):
1044 self.texmessageparsed = keeptexmessageparsed
1045 raise TexResultError("unhandled TeX response (might be an error)", self)
1046 else:
1047 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1049 def finishdvi(self, ignoretail=0):
1050 """finish TeX/LaTeX and read the dvifile
1051 - this method ensures that all textboxes can access their
1052 dvicanvas"""
1053 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1054 dvifilename = "%s.dvi" % self.texfilename
1055 if not self.texipc:
1056 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvidebug)
1057 page = 1
1058 for box in self.needdvitextboxes:
1059 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0], fontmap=box.fontmap))
1060 page += 1
1061 if not ignoretail and self.dvifile.readpage(None) is not None:
1062 raise RuntimeError("end of dvifile expected")
1063 self.dvifile = None
1064 self.needdvitextboxes = []
1066 def reset(self, reinit=0):
1067 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1068 if self.texruns:
1069 self.finishdvi()
1070 if self.texdebug is not None:
1071 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1072 self.executeid = 0
1073 self.page = 0
1074 self.texdone = 0
1075 if reinit:
1076 self.preamblemode = 1
1077 for expr, texmessages in self.preambles:
1078 self.execute(expr, texmessages)
1079 if self.mode == "latex":
1080 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1081 self.preamblemode = 0
1082 else:
1083 self.preambles = []
1084 self.preamblemode = 1
1086 def set(self, mode=_unset,
1087 lfs=_unset,
1088 docclass=_unset,
1089 docopt=_unset,
1090 usefiles=_unset,
1091 waitfortex=_unset,
1092 showwaitfortex=_unset,
1093 texipc=_unset,
1094 texdebug=_unset,
1095 dvidebug=_unset,
1096 errordebug=_unset,
1097 pyxgraphics=_unset,
1098 texmessagesstart=_unset,
1099 texmessagesdocclass=_unset,
1100 texmessagesbegindoc=_unset,
1101 texmessagesend=_unset,
1102 texmessagesdefaultpreamble=_unset,
1103 texmessagesdefaultrun=_unset):
1104 """provide a set command for TeX/LaTeX settings
1105 - TeX/LaTeX must not yet been started
1106 - especially needed for the defaultrunner, where no access to
1107 the constructor is available"""
1108 if self.texruns:
1109 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1110 if mode is not _unset:
1111 mode = mode.lower()
1112 if mode != "tex" and mode != "latex":
1113 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1114 self.mode = mode
1115 if lfs is not _unset:
1116 self.lfs = lfs
1117 if docclass is not _unset:
1118 self.docclass = docclass
1119 if docopt is not _unset:
1120 self.docopt = docopt
1121 if usefiles is not _unset:
1122 self.usefiles = usefiles
1123 if waitfortex is not _unset:
1124 self.waitfortex = waitfortex
1125 if showwaitfortex is not _unset:
1126 self.showwaitfortex = showwaitfortex
1127 if texipc is not _unset:
1128 self.texipc = texipc
1129 if texdebug is not _unset:
1130 if self.texdebug is not None:
1131 self.texdebug.close()
1132 if texdebug[-4:] == ".tex":
1133 self.texdebug = open(texdebug, "w")
1134 else:
1135 self.texdebug = open("%s.tex" % texdebug, "w")
1136 if dvidebug is not _unset:
1137 self.dvidebug = dvidebug
1138 if errordebug is not _unset:
1139 self.errordebug = errordebug
1140 if pyxgraphics is not _unset:
1141 self.pyxgraphics = pyxgraphics
1142 if errordebug is not _unset:
1143 self.errordebug = errordebug
1144 if texmessagesstart is not _unset:
1145 self.texmessagesstart = texmessagesstart
1146 if texmessagesdocclass is not _unset:
1147 self.texmessagesdocclass = texmessagesdocclass
1148 if texmessagesbegindoc is not _unset:
1149 self.texmessagesbegindoc = texmessagesbegindoc
1150 if texmessagesend is not _unset:
1151 self.texmessagesend = texmessagesend
1152 if texmessagesdefaultpreamble is not _unset:
1153 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1154 if texmessagesdefaultrun is not _unset:
1155 self.texmessagesdefaultrun = texmessagesdefaultrun
1157 def preamble(self, expr, texmessages=[]):
1158 r"""put something into the TeX/LaTeX preamble
1159 - in LaTeX, this is done before the \begin{document}
1160 (you might use \AtBeginDocument, when you're in need for)
1161 - it is not allowed to call preamble after calling the
1162 text method for the first time (for LaTeX this is needed
1163 due to \begin{document}; in TeX it is forced for compatibility
1164 (you should be able to switch from TeX to LaTeX, if you want,
1165 without breaking something)
1166 - preamble expressions must not create any dvi output
1167 - args might contain texmessage instances"""
1168 if self.texdone or not self.preamblemode:
1169 raise RuntimeError("preamble calls disabled due to previous text calls")
1170 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1171 self.execute(expr, texmessages)
1172 self.preambles.append((expr, texmessages))
1174 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:")
1176 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None):
1177 """create text by passing expr to TeX/LaTeX
1178 - returns a textbox containing the result from running expr thru TeX/LaTeX
1179 - the box center is set to x, y
1180 - *args may contain attr parameters, namely:
1181 - textattr instances
1182 - texmessage instances
1183 - trafo._trafo instances
1184 - style.fillstyle instances"""
1185 if expr is None:
1186 raise ValueError("None expression is invalid")
1187 if self.texdone:
1188 self.reset(reinit=1)
1189 first = 0
1190 if self.preamblemode:
1191 if self.mode == "latex":
1192 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1193 self.preamblemode = 0
1194 first = 1
1195 textattrs = attr.mergeattrs(textattrs) # perform cleans
1196 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1197 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1198 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1199 textattrs = attr.getattrs(textattrs, [textattr])
1200 # reverse loop over the merged textattrs (last is applied first)
1201 lentextattrs = len(textattrs)
1202 for i in range(lentextattrs):
1203 expr = textattrs[lentextattrs-1-i].apply(expr)
1204 try:
1205 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1206 except TexResultError:
1207 self.finishdvi(ignoretail=1)
1208 raise
1209 if self.texipc:
1210 if first:
1211 self.dvifile = dvifile.DVIfile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1212 match = self.PyXBoxPattern.search(self.texmessage)
1213 if not match or int(match.group("page")) != self.page:
1214 raise TexResultError("box extents not found", self)
1215 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1216 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1217 for t in trafos:
1218 box.reltransform(t) # TODO: should trafos really use reltransform???
1219 # this is quite different from what we do elsewhere!!!
1220 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1221 if self.texipc:
1222 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0], fontmap=fontmap))
1223 else:
1224 box.fontmap = fontmap
1225 self.needdvitextboxes.append(box)
1226 return box
1228 def text_pt(self, x, y, expr, *args, **kwargs):
1229 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1231 PyXVariableBoxPattern = re.compile(r"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1233 def textboxes(self, text, pageshapes):
1234 # this is some experimental code to put text into several boxes
1235 # while the bounding shape changes from box to box (rectangles only)
1236 # first we load sev.tex
1237 if not self.textboxesincluded:
1238 self.execute(r"\input textboxes.tex", [texmessage.load])
1239 self.textboxesincluded = 1
1240 # define page shapes
1241 pageshapes_str = "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit.topt(pageshapes[0][0]), 72.27/72*unit.topt(pageshapes[0][1]))
1242 pageshapes_str += "\\lohsizes={%\n"
1243 for hsize, vsize in pageshapes[1:]:
1244 pageshapes_str += "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(hsize))
1245 pageshapes_str += "{\\relax}%\n}%\n"
1246 pageshapes_str += "\\lovsizes={%\n"
1247 for hsize, vsize in pageshapes[1:]:
1248 pageshapes_str += "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit.topt(vsize))
1249 pageshapes_str += "{\\relax}%\n}%\n"
1250 page = 0
1251 parnos = []
1252 parshapes = []
1253 loop = 0
1254 while 1:
1255 self.execute(pageshapes_str, [])
1256 parnos_str = "}{".join(parnos)
1257 if parnos_str:
1258 parnos_str = "{%s}" % parnos_str
1259 parnos_str = "\\parnos={%s{\\relax}}%%\n" % parnos_str
1260 self.execute(parnos_str, [])
1261 parshapes_str = "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes)
1262 self.execute(parshapes_str, [])
1263 self.execute("\\global\\count0=1%%\n"
1264 "\\global\\parno=0%%\n"
1265 "\\global\\myprevgraf=0%%\n"
1266 "\\global\\showprevgraf=0%%\n"
1267 "\\global\\outputtype=0%%\n"
1268 "\\global\\leastcost=10000000%%\n"
1269 "%s%%\n"
1270 "\\vfill\\supereject%%\n" % text, [texmessage.ignore])
1271 if self.texipc:
1272 if self.dvifile is None:
1273 self.dvifile = dvifile.DVIfile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1274 else:
1275 raise RuntimeError("textboxes currently needs texipc")
1276 lastparnos = parnos
1277 parnos = []
1278 lastparshapes = parshapes
1279 parshapes = []
1280 pages = 0
1281 lastpar = prevgraf = -1
1282 m = self.PyXVariableBoxPattern.search(self.texmessage)
1283 while m:
1284 pages += 1
1285 page = int(m.group("page"))
1286 assert page == pages
1287 par = int(m.group("par"))
1288 prevgraf = int(m.group("prevgraf"))
1289 if page <= len(pageshapes):
1290 width = 72.27/72*unit.topt(pageshapes[page-1][0])
1291 else:
1292 width = 72.27/72*unit.topt(pageshapes[-1][0])
1293 if page < len(pageshapes):
1294 nextwidth = 72.27/72*unit.topt(pageshapes[page][0])
1295 else:
1296 nextwidth = 72.27/72*unit.topt(pageshapes[-1][0])
1298 if par != lastpar:
1299 # a new paragraph is to be broken
1300 parnos.append(str(par))
1301 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf)])
1302 if len(parshape):
1303 parshape = " 0pt " + parshape
1304 parshapes.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf + 1, parshape, nextwidth))
1305 elif prevgraf == lastprevgraf:
1306 pass
1307 else:
1308 # we have to append the breaking of the previous paragraph
1309 oldparshape = " ".join(parshapes[-1].split(' ')[2:2+2*lastprevgraf])
1310 oldparshape = oldparshape.split('}')[0]
1311 if len(parshape):
1312 oldparshape = " " + oldparshape
1313 parshape = " 0pt ".join(["%.5ftruept" % width for i in range(prevgraf - lastprevgraf)])
1314 if len(parshape):
1315 parshape = " 0pt " + parshape
1316 else:
1317 parshape = " "
1318 parshapes[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf + 1, oldparshape, parshape, nextwidth)
1319 lastpar = par
1320 lastprevgraf = prevgraf
1321 nextpos = m.end()
1322 m = self.PyXVariableBoxPattern.search(self.texmessage, nextpos)
1323 result = []
1324 for i in range(pages):
1325 result.append(self.dvifile.readpage([i + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1326 if parnos == lastparnos and parshapes == lastparshapes:
1327 return result
1328 loop += 1
1329 if loop > 100:
1330 raise TexResultError("Too many loops in textboxes ", texrunner)
1333 # the module provides an default texrunner and methods for direct access
1334 defaulttexrunner = texrunner()
1335 reset = defaulttexrunner.reset
1336 set = defaulttexrunner.set
1337 preamble = defaulttexrunner.preamble
1338 text = defaulttexrunner.text
1339 text_pt = defaulttexrunner.text_pt
1341 def escapestring(s, replace={" ": "~",
1342 "$": "\\$",
1343 "&": "\\&",
1344 "#": "\\#",
1345 "_": "\\_",
1346 "%": "\\%",
1347 "^": "\\string^",
1348 "~": "\\string~",
1349 "<": "{$<$}",
1350 ">": "{$>$}",
1351 "{": "{$\{$}",
1352 "}": "{$\}$}",
1353 "\\": "{$\setminus$}",
1354 "|": "{$\mid$}"}):
1355 "escape all ascii characters such that they are printable by TeX/LaTeX"
1356 i = 0
1357 while i < len(s):
1358 if not 32 <= ord(s[i]) < 127:
1359 raise ValueError("escapestring function handles ascii strings only")
1360 c = s[i]
1361 try:
1362 r = replace[c]
1363 except KeyError:
1364 i += 1
1365 else:
1366 s = s[:i] + r + s[i+1:]
1367 i += len(r)
1368 return s