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