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