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