fix plot style breakage
[PyX/mjg.git] / pyx / text.py
blob0b9881af8baf40ec92dc665711878cfb58c8cf0b
1 # -*- encoding: utf-8 -*-
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-2011 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, unit, box, canvas, trafo, version, attr, style, filelocator, pycompat, path
26 from pyx.dvi import dvifile
27 import bbox as bboxmodule
29 class PyXTeXWarning(UserWarning): pass
30 warnings.filterwarnings('always', category=PyXTeXWarning)
32 ###############################################################################
33 # texmessages
34 # - please don't get confused:
35 # - there is a texmessage (and a texmessageparsed) attribute within the
36 # texrunner; it contains TeX/LaTeX response from the last command execution
37 # - instances of classes derived from the class texmessage are used to
38 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
39 # attribute of a texrunner instance
40 # - the multiple usage of the name texmessage might be removed in the future
41 # - texmessage instances should implement _Itexmessage
42 ###############################################################################
44 class TexResultError(RuntimeError):
45 """specialized texrunner exception class
46 - it is raised by texmessage instances, when a texmessage indicates an error
47 - it is raised by the texrunner itself, whenever there is a texmessage left
48 after all parsing of this message (by texmessage instances)
49 prints a detailed report about the problem
50 - the verbose level is controlled by texrunner.errordebug"""
52 def __init__(self, description, texrunner):
53 if texrunner.errordebug >= 2:
54 self.description = ("%s\n" % description +
55 "The expression passed to TeX was:\n"
56 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
57 "The return message from TeX was:\n"
58 " %s\n" % texrunner.texmessage.replace("\n", "\n ").rstrip() +
59 "After parsing this message, the following was left:\n"
60 " %s" % texrunner.texmessageparsed.replace("\n", "\n ").rstrip())
61 elif texrunner.errordebug == 1:
62 firstlines = texrunner.texmessageparsed.split("\n")
63 if len(firstlines) > 5:
64 firstlines = firstlines[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
65 self.description = ("%s\n" % description +
66 "The expression passed to TeX was:\n"
67 " %s\n" % texrunner.expr.replace("\n", "\n ").rstrip() +
68 "After parsing the return message from TeX, the following was left:\n" +
69 reduce(lambda x, y: "%s %s\n" % (x,y), firstlines, "").rstrip())
70 else:
71 self.description = description
73 def __str__(self):
74 return self.description
77 class _Itexmessage:
78 """validates/invalidates TeX/LaTeX response"""
80 def check(self, texrunner):
81 """check a Tex/LaTeX response and respond appropriate
82 - read the texrunners texmessageparsed attribute
83 - if there is an problem found, raise TexResultError
84 - remove any valid and identified TeX/LaTeX response
85 from the texrunners texmessageparsed attribute
86 -> finally, there should be nothing left in there,
87 otherwise it is interpreted as an error"""
90 class texmessage(attr.attr): pass
93 class _texmessagestart(texmessage):
94 """validates TeX/LaTeX startup"""
96 __implements__ = _Itexmessage
98 startpattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
100 def check(self, texrunner):
101 # check for "This is e-TeX"
102 m = self.startpattern.search(texrunner.texmessageparsed)
103 if not m:
104 raise TexResultError("TeX startup failed", texrunner)
105 texrunner.texmessageparsed = texrunner.texmessageparsed[m.end():]
107 # check for \raiseerror -- just to be sure that communication works
108 try:
109 texrunner.texmessageparsed = texrunner.texmessageparsed.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
110 except (IndexError, ValueError):
111 raise TexResultError("TeX scrollmode check failed", texrunner)
114 class _texmessagenofile(texmessage):
115 """allows for LaTeXs no-file warning"""
117 __implements__ = _Itexmessage
119 def __init__(self, fileending):
120 self.fileending = fileending
122 def check(self, texrunner):
123 try:
124 s1, s2 = texrunner.texmessageparsed.split("No file %s.%s." % (texrunner.texfilename, self.fileending), 1)
125 texrunner.texmessageparsed = s1 + s2
126 except (IndexError, ValueError):
127 try:
128 s1, s2 = texrunner.texmessageparsed.split("No file %s%s%s.%s." % (os.curdir,
129 os.sep,
130 texrunner.texfilename,
131 self.fileending), 1)
132 texrunner.texmessageparsed = s1 + s2
133 except (IndexError, ValueError):
134 pass
137 class _texmessageinputmarker(texmessage):
138 """validates the PyXInputMarker"""
140 __implements__ = _Itexmessage
142 def check(self, texrunner):
143 try:
144 s1, s2 = texrunner.texmessageparsed.split("PyXInputMarker:executeid=%s:" % texrunner.executeid, 1)
145 texrunner.texmessageparsed = s1 + s2
146 except (IndexError, ValueError):
147 raise TexResultError("PyXInputMarker expected", texrunner)
150 class _texmessagepyxbox(texmessage):
151 """validates the PyXBox output"""
153 __implements__ = _Itexmessage
155 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:")
157 def check(self, texrunner):
158 m = self.pattern.search(texrunner.texmessageparsed)
159 if m and m.group("page") == str(texrunner.page):
160 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
161 else:
162 raise TexResultError("PyXBox expected", texrunner)
165 class _texmessagepyxpageout(texmessage):
166 """validates the dvi shipout message (writing a page to the dvi file)"""
168 __implements__ = _Itexmessage
170 def check(self, texrunner):
171 try:
172 s1, s2 = texrunner.texmessageparsed.split("[80.121.88.%s]" % texrunner.page, 1)
173 texrunner.texmessageparsed = s1 + s2
174 except (IndexError, ValueError):
175 raise TexResultError("PyXPageOutMarker expected", texrunner)
178 class _texmessageend(texmessage):
179 """validates TeX/LaTeX finish"""
181 __implements__ = _Itexmessage
183 auxPattern = re.compile(r"\(([^()]+\.aux|\"[^\"]+\.aux\")\)")
185 def check(self, texrunner):
186 m = self.auxPattern.search(texrunner.texmessageparsed)
187 if m:
188 texrunner.texmessageparsed = (texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]).strip()
190 # check for "(see the transcript file for additional information)"
191 try:
192 s1, s2 = texrunner.texmessageparsed.split("(see the transcript file for additional information)", 1)
193 texrunner.texmessageparsed = (s1 + s2).strip()
194 except (IndexError, ValueError):
195 pass
197 # check for "Output written on ...dvi (1 page, 220 bytes)."
198 dvipattern = re.compile(r"Output written on %s\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\." % texrunner.texfilename)
199 m = dvipattern.search(texrunner.texmessageparsed)
200 if texrunner.page:
201 if not m:
202 raise TexResultError("TeX dvifile messages expected", texrunner)
203 if m.group("page") != str(texrunner.page):
204 raise TexResultError("wrong number of pages reported", texrunner)
205 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
206 else:
207 try:
208 s1, s2 = texrunner.texmessageparsed.split("No pages of output.", 1)
209 texrunner.texmessageparsed = s1 + s2
210 except (IndexError, ValueError):
211 raise TexResultError("no dvifile expected", texrunner)
213 # check for "Transcript written on ...log."
214 try:
215 s1, s2 = texrunner.texmessageparsed.split("Transcript written on %s.log." % texrunner.texfilename, 1)
216 texrunner.texmessageparsed = s1 + s2
217 except (IndexError, ValueError):
218 raise TexResultError("TeX logfile message expected", texrunner)
221 class _texmessageemptylines(texmessage):
222 """validates "*-only" (TeX/LaTeX input marker in interactive mode) and empty lines
223 also clear TeX interactive mode warning (Please type a command or say `\\end')
226 __implements__ = _Itexmessage
228 def check(self, texrunner):
229 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please type a command or say `\end')", "")
230 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
231 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
232 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
235 class _texmessageload(texmessage):
236 """validates inclusion of arbitrary files
237 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
238 <filename> is a readable file and other stuff can be anything
239 - If the filename is enclosed in double quotes, it may contain blank space.
240 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
241 - this is not always wanted, but we just assume that file inclusion is fine"""
243 __implements__ = _Itexmessage
245 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?<!\")[^()\s\n]+(?!\"))|[^\"\n]+)[\"]?(?P<additional>[^()]*)\)")
247 def baselevels(self, s, maxlevel=1, brackets="()", quotes='""'):
248 """strip parts of a string above a given bracket level
249 - return a modified (some parts might be removed) version of the string s
250 where all parts inside brackets with level higher than maxlevel are
251 removed
252 - if brackets do not match (number of left and right brackets is wrong
253 or at some points there were more right brackets than left brackets)
254 just return the unmodified string
255 - a quoted string immediately followed after a bracket is left untouched
256 even if it contains quotes itself"""
257 level = 0
258 highestlevel = 0
259 inquote = 0
260 res = ""
261 for i, c in enumerate(s):
262 if quotes and level <= maxlevel:
263 if not inquote and c == quotes[0] and i and s[i-1] == brackets[0]:
264 inquote = 1
265 elif inquote and c == quotes[1]:
266 inquote = 0
267 if inquote:
268 res += c
269 else:
270 if c == brackets[0]:
271 level += 1
272 if level > highestlevel:
273 highestlevel = level
274 if level <= maxlevel:
275 res += c
276 if c == brackets[1]:
277 level -= 1
278 if level == 0 and highestlevel > 0:
279 return res
281 def check(self, texrunner):
282 search = self.baselevels(texrunner.texmessageparsed)
283 res = []
284 if search is not None:
285 m = self.pattern.search(search)
286 while m:
287 filename = m.group("filename").replace("\n", "")
288 try:
289 additional = m.group("additional")
290 except IndexError:
291 additional = ""
292 if (os.access(filename, os.R_OK) or
293 len(additional) and additional[0] == "\n" and os.access(filename+additional.split()[0], os.R_OK)):
294 res.append(search[:m.start()])
295 else:
296 res.append(search[:m.end()])
297 search = search[m.end():]
298 m = self.pattern.search(search)
299 else:
300 res.append(search)
301 texrunner.texmessageparsed = "".join(res)
304 class _texmessageloaddef(_texmessageload):
305 """validates the inclusion of font description files (fd-files)
306 - works like _texmessageload
307 - filename must end with .def or .fd
308 - further text is allowed"""
310 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?:(?<!\")[^\(\)\s\n\"]+)|(?:(?<=\")[^\(\)\"]+))(\.fd|\.def))[\"]?[\s\n]*(?P<additional>[\(]?[^\(\)]*[\)]?)[\s\n]*\)")
312 def baselevels(self, s, **kwargs):
313 return s
316 class _texmessagegraphicsload(_texmessageload):
317 """validates the inclusion of files as the graphics packages writes it
318 - works like _texmessageload, but using "<" and ">" as delimiters
319 - filename must end with .eps and no further text is allowed"""
321 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
323 def baselevels(self, s, **kwargs):
324 return s
327 class _texmessageignore(_texmessageload):
328 """validates any TeX/LaTeX response
329 - this might be used, when the expression is ok, but no suitable texmessage
330 parser is available
331 - PLEASE: - consider writing suitable tex message parsers
332 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
334 __implements__ = _Itexmessage
336 def check(self, texrunner):
337 texrunner.texmessageparsed = ""
340 texmessage.start = _texmessagestart()
341 texmessage.noaux = _texmessagenofile("aux")
342 texmessage.nonav = _texmessagenofile("nav")
343 texmessage.end = _texmessageend()
344 texmessage.load = _texmessageload()
345 texmessage.loaddef = _texmessageloaddef()
346 texmessage.graphicsload = _texmessagegraphicsload()
347 texmessage.ignore = _texmessageignore()
349 # for internal use:
350 texmessage.inputmarker = _texmessageinputmarker()
351 texmessage.pyxbox = _texmessagepyxbox()
352 texmessage.pyxpageout = _texmessagepyxpageout()
353 texmessage.emptylines = _texmessageemptylines()
356 class _texmessageallwarning(texmessage):
357 """validates a given pattern 'pattern' as a warning 'warning'"""
359 def check(self, texrunner):
360 if texrunner.texmessageparsed:
361 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
362 texrunner.texmessageparsed = ""
364 texmessage.allwarning = _texmessageallwarning()
367 class texmessagepattern(texmessage):
368 """validates a given pattern and issue a warning (when set)"""
370 def __init__(self, pattern, warning=None):
371 self.pattern = pattern
372 self.warning = warning
374 def check(self, texrunner):
375 m = self.pattern.search(texrunner.texmessageparsed)
376 while m:
377 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
378 if self.warning:
379 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
380 m = self.pattern.search(texrunner.texmessageparsed)
382 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "ignoring font warning")
383 texmessage.boxwarning = texmessagepattern(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "ignoring overfull/underfull box warning")
384 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")
385 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")
386 texmessage.nobblwarning = texmessagepattern(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE), "ignoring no-bbl warning")
390 ###############################################################################
391 # textattrs
392 ###############################################################################
394 _textattrspreamble = ""
396 class textattr:
397 "a textattr defines a apply method, which modifies a (La)TeX expression"
399 class _localattr: pass
401 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
402 \def\PyXragged{%
403 \leftskip=0pt plus \PyXFlushHAlign fil%
404 \rightskip=0pt plus 1fil%
405 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
406 \parfillskip=0pt%
407 \pretolerance=9999%
408 \tolerance=9999%
409 \parindent=0pt%
410 \hyphenpenalty=9999%
411 \exhyphenpenalty=9999}%
414 class boxhalign(attr.exclusiveattr, textattr, _localattr):
416 def __init__(self, aboxhalign):
417 self.boxhalign = aboxhalign
418 attr.exclusiveattr.__init__(self, boxhalign)
420 def apply(self, expr):
421 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
423 boxhalign.left = boxhalign(0)
424 boxhalign.center = boxhalign(0.5)
425 boxhalign.right = boxhalign(1)
426 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
429 class flushhalign(attr.exclusiveattr, textattr, _localattr):
431 def __init__(self, aflushhalign):
432 self.flushhalign = aflushhalign
433 attr.exclusiveattr.__init__(self, flushhalign)
435 def apply(self, expr):
436 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
438 flushhalign.left = flushhalign(0)
439 flushhalign.center = flushhalign(0.5)
440 flushhalign.right = flushhalign(1)
441 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
444 class halign(attr.exclusiveattr, textattr, boxhalign, flushhalign, _localattr):
446 def __init__(self, aboxhalign, aflushhalign):
447 self.boxhalign = aboxhalign
448 self.flushhalign = aflushhalign
449 attr.exclusiveattr.__init__(self, halign)
451 def apply(self, expr):
452 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
454 halign.left = halign(0, 0)
455 halign.center = halign(0.5, 0.5)
456 halign.right = halign(1, 1)
457 halign.clear = attr.clearclass(halign)
458 halign.boxleft = boxhalign.left
459 halign.boxcenter = boxhalign.center
460 halign.boxright = boxhalign.right
461 halign.flushleft = halign.raggedright = flushhalign.left
462 halign.flushcenter = halign.raggedcenter = flushhalign.center
463 halign.flushright = halign.raggedleft = flushhalign.right
466 class _mathmode(attr.attr, textattr, _localattr):
467 "math mode"
469 def apply(self, expr):
470 return r"$\displaystyle{%s}$" % expr
472 mathmode = _mathmode()
473 clearmathmode = attr.clearclass(_mathmode)
476 class _phantom(attr.attr, textattr, _localattr):
477 "phantom text"
479 def apply(self, expr):
480 return r"\phantom{%s}" % expr
482 phantom = _phantom()
483 clearphantom = attr.clearclass(_phantom)
486 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
488 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
490 top = 1
491 middle = 2
492 bottom = 3
494 def __init__(self, width, baseline=top):
495 self.width = width * 72.27 / (unit.scale["x"] * 72)
496 self.baseline = baseline
497 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
499 def apply(self, expr):
500 if self.baseline == self.top:
501 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
502 elif self.baseline == self.middle:
503 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)
504 elif self.baseline == self.bottom:
505 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
506 else:
507 RuntimeError("invalid baseline argument")
509 parbox_pt.clear = attr.clearclass(parbox_pt)
511 class parbox(parbox_pt):
513 def __init__(self, width, **kwargs):
514 parbox_pt.__init__(self, unit.topt(width), **kwargs)
516 parbox.clear = parbox_pt.clear
519 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
521 class valign(attr.sortbeforeexclusiveattr, textattr):
523 def __init__(self, avalign):
524 self.valign = avalign
525 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
527 def apply(self, expr):
528 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)
530 valign.top = valign(0)
531 valign.middle = valign(0.5)
532 valign.bottom = valign(1)
533 valign.clear = valign.baseline = attr.clearclass(valign)
536 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
538 class _vshift(attr.sortbeforeattr, textattr):
540 def __init__(self):
541 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
543 def apply(self, expr):
544 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
546 class vshift(_vshift):
547 "vertical down shift by a fraction of a character height"
549 def __init__(self, lowerratio, heightstr="0"):
550 _vshift.__init__(self)
551 self.lowerratio = lowerratio
552 self.heightstr = heightstr
554 def setheightexpr(self):
555 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
557 class _vshiftmathaxis(_vshift):
558 "vertical down shift by the height of the math axis"
560 def setheightexpr(self):
561 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
564 vshift.bottomzero = vshift(0)
565 vshift.middlezero = vshift(0.5)
566 vshift.topzero = vshift(1)
567 vshift.mathaxis = _vshiftmathaxis()
568 vshift.clear = attr.clearclass(_vshift)
571 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
572 None, "tiny", "scriptsize", "footnotesize", "small"]
574 class size(attr.sortbeforeattr, textattr):
575 "font size"
577 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
578 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
579 raise RuntimeError("either specify sizeindex or sizename")
580 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
581 if sizeindex is not None:
582 if sizeindex >= 0 and sizeindex < sizelist.index(None):
583 self.size = sizelist[sizeindex]
584 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
585 self.size = sizelist[sizeindex]
586 else:
587 raise IndexError("index out of sizelist range")
588 else:
589 self.size = sizename
591 def apply(self, expr):
592 return r"\%s{}%s" % (self.size, expr)
594 size.tiny = size(-4)
595 size.scriptsize = size.script = size(-3)
596 size.footnotesize = size.footnote = size(-2)
597 size.small = size(-1)
598 size.normalsize = size.normal = size(0)
599 size.large = size(1)
600 size.Large = size(2)
601 size.LARGE = size(3)
602 size.huge = size(4)
603 size.Huge = size(5)
604 size.clear = attr.clearclass(size)
607 ###############################################################################
608 # texrunner
609 ###############################################################################
612 class _readpipe(threading.Thread):
613 """threaded reader of TeX/LaTeX output
614 - sets an event, when a specific string in the programs output is found
615 - sets an event, when the terminal ends"""
617 def __init__(self, pipe, expectqueue, gotevent, gotqueue, quitevent):
618 """initialize the reader
619 - pipe: file to be read from
620 - expectqueue: keeps the next InputMarker to be wait for
621 - gotevent: the "got InputMarker" event
622 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
623 - quitevent: the "end of terminal" event"""
624 threading.Thread.__init__(self)
625 self.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
626 self.pipe = pipe
627 self.expectqueue = expectqueue
628 self.gotevent = gotevent
629 self.gotqueue = gotqueue
630 self.quitevent = quitevent
631 self.expect = None
633 def run(self):
634 """thread routine"""
635 def _read():
636 # catch interupted system call errors while reading
637 while 1:
638 try:
639 return self.pipe.readline()
640 except IOError, e:
641 if e.errno != errno.EINTR:
642 raise
643 read = _read() # read, what comes in
644 try:
645 self.expect = self.expectqueue.get_nowait() # read, what should be expected
646 except Queue.Empty:
647 pass
648 while len(read):
649 # universal EOL handling (convert everything into unix like EOLs)
650 # XXX is this necessary on pipes?
651 read = read.replace("\r", "").replace("\n", "") + "\n"
652 self.gotqueue.put(read) # report, whats read
653 if self.expect is not None and read.find(self.expect) != -1:
654 self.gotevent.set() # raise the got event, when the output was expected (XXX: within a single line)
655 read = _read() # read again
656 try:
657 self.expect = self.expectqueue.get_nowait()
658 except Queue.Empty:
659 pass
660 # EOF reached
661 self.pipe.close()
662 if self.expect is not None and self.expect.find("PyXInputMarker") != -1:
663 raise RuntimeError("TeX/LaTeX finished unexpectedly")
664 self.quitevent.set()
667 class textbox(box.rect, canvas._canvas):
668 """basically a box.rect, but it contains a text created by the texrunner
669 - texrunner._text and texrunner.text return such an object
670 - _textbox instances can be inserted into a canvas
671 - the output is contained in a page of the dvifile available thru the texrunner"""
672 # TODO: shouldn't all boxes become canvases? how about inserts then?
674 def __init__(self, x, y, left, right, height, depth, finishdvi, attrs):
676 - finishdvi is a method to be called to get the dvicanvas
677 (e.g. the finishdvi calls the setdvicanvas method)
678 - attrs are fillstyles"""
679 self.left = left
680 self.right = right
681 self.width = left + right
682 self.height = height
683 self.depth = depth
684 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
685 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
686 canvas._canvas.__init__(self, attrs)
687 self.finishdvi = finishdvi
688 self.dvicanvas = None
689 self.insertdvicanvas = 0
691 def transform(self, *trafos):
692 if self.insertdvicanvas:
693 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
694 box.rect.transform(self, *trafos)
695 for trafo in trafos:
696 self.texttrafo = trafo * self.texttrafo
698 def setdvicanvas(self, dvicanvas):
699 if self.dvicanvas is not None:
700 raise RuntimeError("multiple call to setdvicanvas")
701 self.dvicanvas = dvicanvas
703 def ensuredvicanvas(self):
704 if self.dvicanvas is None:
705 self.finishdvi()
706 assert self.dvicanvas is not None, "finishdvi is broken"
707 if not self.insertdvicanvas:
708 self.insert(self.dvicanvas, [self.texttrafo])
709 self.insertdvicanvas = 1
711 def marker(self, marker):
712 self.ensuredvicanvas()
713 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
715 def textpath(self):
716 self.ensuredvicanvas()
717 textpath = path.path()
718 for item in self.dvicanvas.items:
719 try:
720 textpath += item.textpath()
721 except AttributeError:
722 # ignore color settings etc.
723 pass
724 return textpath.transformed(self.texttrafo)
726 def processPS(self, file, writer, context, registry, bbox):
727 self.ensuredvicanvas()
728 abbox = bboxmodule.empty()
729 canvas._canvas.processPS(self, file, writer, context, registry, abbox)
730 bbox += box.rect.bbox(self)
732 def processPDF(self, file, writer, context, registry, bbox):
733 self.ensuredvicanvas()
734 abbox = bboxmodule.empty()
735 canvas._canvas.processPDF(self, file, writer, context, registry, abbox)
736 bbox += box.rect.bbox(self)
739 def _cleantmp(texrunner):
740 """get rid of temporary files
741 - function to be registered by atexit
742 - files contained in usefiles are kept"""
743 if texrunner.texruns: # cleanup while TeX is still running?
744 texrunner.expectqueue.put_nowait(None) # do not expect any output anymore
745 if texrunner.mode == "latex": # try to immediately quit from TeX or LaTeX
746 texrunner.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
747 else:
748 texrunner.texinput.write("\n\\end\n")
749 texrunner.texinput.close() # close the input queue and
750 if not texrunner.waitforevent(texrunner.quitevent): # wait for finish of the output
751 return # didn't got a quit from TeX -> we can't do much more
752 texrunner.texruns = 0
753 texrunner.texdone = 1
754 for usefile in texrunner.usefiles:
755 extpos = usefile.rfind(".")
756 try:
757 os.rename(texrunner.texfilename + usefile[extpos:], usefile)
758 except OSError:
759 pass
760 for file in glob.glob("%s.*" % texrunner.texfilename) + ["%sNotes.bib" % texrunner.texfilename]:
761 try:
762 os.unlink(file)
763 except OSError:
764 pass
765 if texrunner.texdebug is not None:
766 try:
767 texrunner.texdebug.close()
768 texrunner.texdebug = None
769 except IOError:
770 pass
773 class _unset:
774 pass
776 class texrunner:
777 """TeX/LaTeX interface
778 - runs TeX/LaTeX expressions instantly
779 - checks TeX/LaTeX response
780 - the instance variable texmessage stores the last TeX
781 response as a string
782 - the instance variable texmessageparsed stores a parsed
783 version of texmessage; it should be empty after
784 texmessage.check was called, otherwise a TexResultError
785 is raised
786 - the instance variable errordebug controls the verbose
787 level of TexResultError"""
789 defaulttexmessagesstart = [texmessage.start]
790 defaulttexmessagesdocclass = [texmessage.load]
791 defaulttexmessagesbegindoc = [texmessage.load, texmessage.noaux]
792 defaulttexmessagesend = [texmessage.end, texmessage.fontwarning, texmessage.rerunwarning, texmessage.nobblwarning]
793 defaulttexmessagesdefaultpreamble = [texmessage.load]
794 defaulttexmessagesdefaultrun = [texmessage.loaddef, texmessage.graphicsload,
795 texmessage.fontwarning, texmessage.boxwarning, texmessage.packagewarning]
797 def __init__(self, mode="tex",
798 lfs="10pt",
799 docclass="article",
800 docopt=None,
801 usefiles=[],
802 waitfortex=config.getint("text", "waitfortex", 60),
803 showwaitfortex=config.getint("text", "showwaitfortex", 5),
804 texipc=config.getboolean("text", "texipc", 0),
805 texdebug=None,
806 dvidebug=0,
807 errordebug=1,
808 pyxgraphics=1,
809 texmessagesstart=[],
810 texmessagesdocclass=[],
811 texmessagesbegindoc=[],
812 texmessagesend=[],
813 texmessagesdefaultpreamble=[],
814 texmessagesdefaultrun=[]):
815 mode = mode.lower()
816 if mode != "tex" and mode != "latex":
817 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
818 self.mode = mode
819 self.lfs = lfs
820 self.docclass = docclass
821 self.docopt = docopt
822 self.usefiles = usefiles[:]
823 self.waitfortex = waitfortex
824 self.showwaitfortex = showwaitfortex
825 self.texipc = texipc
826 if texdebug is not None:
827 if texdebug[-4:] == ".tex":
828 self.texdebug = open(texdebug, "w")
829 else:
830 self.texdebug = open("%s.tex" % texdebug, "w")
831 else:
832 self.texdebug = None
833 self.dvidebug = dvidebug
834 self.errordebug = errordebug
835 self.pyxgraphics = pyxgraphics
836 self.texmessagesstart = texmessagesstart[:]
837 self.texmessagesdocclass = texmessagesdocclass[:]
838 self.texmessagesbegindoc = texmessagesbegindoc[:]
839 self.texmessagesend = texmessagesend[:]
840 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble[:]
841 self.texmessagesdefaultrun = texmessagesdefaultrun[:]
843 self.texruns = 0
844 self.texdone = 0
845 self.preamblemode = 1
846 self.executeid = 0
847 self.page = 0
848 self.preambles = []
849 self.needdvitextboxes = [] # when texipc-mode off
850 self.dvifile = None
851 self.textboxesincluded = 0
852 savetempdir = tempfile.tempdir
853 tempfile.tempdir = os.curdir
854 self.texfilename = os.path.basename(tempfile.mktemp())
855 tempfile.tempdir = savetempdir
857 def waitforevent(self, event):
858 """waits verbosely with an timeout for an event
859 - observes an event while periodly while printing messages
860 - returns the status of the event (isSet)
861 - does not clear the event"""
862 if self.showwaitfortex:
863 waited = 0
864 hasevent = 0
865 while waited < self.waitfortex and not hasevent:
866 if self.waitfortex - waited > self.showwaitfortex:
867 event.wait(self.showwaitfortex)
868 waited += self.showwaitfortex
869 else:
870 event.wait(self.waitfortex - waited)
871 waited += self.waitfortex - waited
872 hasevent = event.isSet()
873 if not hasevent:
874 if waited < self.waitfortex:
875 warnings.warn("still waiting for %s after %i (of %i) seconds..." % (self.mode, waited, self.waitfortex), PyXTeXWarning)
876 else:
877 warnings.warn("the timeout of %i seconds expired and %s did not respond." % (waited, self.mode), PyXTeXWarning)
878 return hasevent
879 else:
880 event.wait(self.waitfortex)
881 return event.isSet()
883 def execute(self, expr, texmessages):
884 """executes expr within TeX/LaTeX
885 - if self.texruns is not yet set, TeX/LaTeX is initialized,
886 self.texruns is set and self.preamblemode is set
887 - the method must not be called, when self.texdone is already set
888 - expr should be a string or None
889 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
890 self.texdone becomes set
891 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
892 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
893 - texmessages is a list of texmessage instances"""
894 if not self.texruns:
895 if self.texdebug is not None:
896 self.texdebug.write("%% PyX %s texdebug file\n" % version.version)
897 self.texdebug.write("%% mode: %s\n" % self.mode)
898 self.texdebug.write("%% date: %s\n" % time.asctime(time.localtime(time.time())))
899 for usefile in self.usefiles:
900 extpos = usefile.rfind(".")
901 try:
902 os.rename(usefile, self.texfilename + usefile[extpos:])
903 except OSError:
904 pass
905 texfile = open("%s.tex" % self.texfilename, "w") # start with filename -> creates dvi file with that name
906 texfile.write("\\relax%\n")
907 texfile.close()
908 if self.texipc:
909 ipcflag = " --ipc"
910 else:
911 ipcflag = ""
912 try:
913 self.texinput, self.texoutput = pycompat.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t", 0)
914 except ValueError:
915 # workaround: bufsize = 0 is not supported on MS windows for os.open4 (Python 2.4 and below, i.e. where subprocess is not available)
916 self.texinput, self.texoutput = pycompat.popen4("%s%s %s" % (self.mode, ipcflag, self.texfilename), "t")
917 atexit.register(_cleantmp, self)
918 self.expectqueue = Queue.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
919 self.gotevent = threading.Event() # keeps the got inputmarker event
920 self.gotqueue = Queue.Queue(0) # allow arbitrary number of entries
921 self.quitevent = threading.Event() # keeps for end of terminal event
922 self.readoutput = _readpipe(self.texoutput, self.expectqueue, self.gotevent, self.gotqueue, self.quitevent)
923 self.texruns = 1
924 oldpreamblemode = self.preamblemode
925 self.preamblemode = 1
926 self.readoutput.start()
927 self.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
928 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
929 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
930 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
931 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
932 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
933 "\\newdimen\\PyXDimenHAlignRT%\n" +
934 _textattrspreamble + # insert preambles for textattrs macros
935 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
936 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
937 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
938 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
939 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
940 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
941 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
942 "lt=\\the\\PyXDimenHAlignLT,"
943 "rt=\\the\\PyXDimenHAlignRT,"
944 "ht=\\the\\ht\\PyXBox,"
945 "dp=\\the\\dp\\PyXBox:}%\n"
946 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
947 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
948 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
949 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
950 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
951 self.defaulttexmessagesstart + self.texmessagesstart)
952 os.remove("%s.tex" % self.texfilename)
953 if self.mode == "tex":
954 if self.lfs:
955 if not self.lfs.endswith(".lfs"):
956 self.lfs = "%s.lfs" % self.lfs
957 lfsfile = filelocator.open(self.lfs, [], "r")
958 lfsdef = lfsfile.read()
959 lfsfile.close()
960 self.execute(lfsdef, [])
961 self.execute("\\normalsize%\n", [])
962 self.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
963 elif self.mode == "latex":
964 if self.pyxgraphics:
965 pyxdef = filelocator.open("pyx.def", [], "rb")
966 pyxdef_filename = self.texfilename + ".pyx.def"
967 pyxdef_file = open(pyxdef_filename, "wb")
968 pyxdef_file.write(pyxdef.read())
969 pyxdef.close()
970 pyxdef_file.close()
971 pyxdef_filename_tex = os.path.abspath(pyxdef_filename).replace(os.sep, "/")
972 self.execute("\\makeatletter%\n"
973 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
974 "\\def\\ProcessOptions{%\n"
975 "\\def\\Gin@driver{" + pyxdef_filename_tex + "}%\n"
976 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
977 "\\saveProcessOptions}%\n"
978 "\\makeatother",
980 if self.docopt is not None:
981 self.execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
982 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
983 else:
984 self.execute("\\documentclass{%s}" % self.docclass,
985 self.defaulttexmessagesdocclass + self.texmessagesdocclass)
986 self.preamblemode = oldpreamblemode
987 self.executeid += 1
988 if expr is not None: # TeX/LaTeX should process expr
989 self.expectqueue.put_nowait("PyXInputMarker:executeid=%i:" % self.executeid)
990 if self.preamblemode:
991 self.expr = ("%s%%\n" % expr +
992 "\\PyXInput{%i}%%\n" % self.executeid)
993 else:
994 self.page += 1
995 self.expr = ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr, self.page) +
996 "\\PyXInput{%i}%%\n" % self.executeid)
997 else: # TeX/LaTeX should be finished
998 self.expectqueue.put_nowait("Transcript written on %s.log" % self.texfilename)
999 if self.mode == "latex":
1000 self.expr = "\\end{document}%\n"
1001 else:
1002 self.expr = "\\end%\n"
1003 if self.texdebug is not None:
1004 self.texdebug.write(self.expr)
1005 self.texinput.write(self.expr)
1006 gotevent = self.waitforevent(self.gotevent)
1007 self.gotevent.clear()
1008 if expr is None and gotevent: # TeX/LaTeX should have finished
1009 self.texruns = 0
1010 self.texdone = 1
1011 self.texinput.close() # close the input queue and
1012 gotevent = self.waitforevent(self.quitevent) # wait for finish of the output
1013 try:
1014 self.texmessage = ""
1015 while 1:
1016 self.texmessage += self.gotqueue.get_nowait()
1017 except Queue.Empty:
1018 pass
1019 self.texmessage = self.texmessage.replace("\r\n", "\n").replace("\r", "\n")
1020 self.texmessageparsed = self.texmessage
1021 if gotevent:
1022 if expr is not None:
1023 texmessage.inputmarker.check(self)
1024 if not self.preamblemode:
1025 texmessage.pyxbox.check(self)
1026 texmessage.pyxpageout.check(self)
1027 texmessages = attr.mergeattrs(texmessages)
1028 for t in texmessages:
1029 t.check(self)
1030 keeptexmessageparsed = self.texmessageparsed
1031 texmessage.emptylines.check(self)
1032 if len(self.texmessageparsed):
1033 self.texmessageparsed = keeptexmessageparsed
1034 raise TexResultError("unhandled TeX response (might be an error)", self)
1035 else:
1036 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self.waitfortex, self)
1038 def finishdvi(self, ignoretail=0):
1039 """finish TeX/LaTeX and read the dvifile
1040 - this method ensures that all textboxes can access their
1041 dvicanvas"""
1042 self.execute(None, self.defaulttexmessagesend + self.texmessagesend)
1043 dvifilename = "%s.dvi" % self.texfilename
1044 if not self.texipc:
1045 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvidebug)
1046 page = 1
1047 for box in self.needdvitextboxes:
1048 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0], fontmap=box.fontmap, singlecharmode=box.singlecharmode))
1049 page += 1
1050 if not ignoretail and self.dvifile.readpage(None) is not None:
1051 raise RuntimeError("end of dvifile expected")
1052 self.dvifile = None
1053 self.needdvitextboxes = []
1055 def reset(self, reinit=0):
1056 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1057 if self.texruns:
1058 self.finishdvi()
1059 if self.texdebug is not None:
1060 self.texdebug.write("%s\n%% preparing restart of %s\n" % ("%"*80, self.mode))
1061 self.executeid = 0
1062 self.page = 0
1063 self.texdone = 0
1064 if reinit:
1065 self.preamblemode = 1
1066 for expr, texmessages in self.preambles:
1067 self.execute(expr, texmessages)
1068 if self.mode == "latex":
1069 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1070 self.preamblemode = 0
1071 else:
1072 self.preambles = []
1073 self.preamblemode = 1
1075 def set(self, mode=_unset,
1076 lfs=_unset,
1077 docclass=_unset,
1078 docopt=_unset,
1079 usefiles=_unset,
1080 waitfortex=_unset,
1081 showwaitfortex=_unset,
1082 texipc=_unset,
1083 texdebug=_unset,
1084 dvidebug=_unset,
1085 errordebug=_unset,
1086 pyxgraphics=_unset,
1087 texmessagesstart=_unset,
1088 texmessagesdocclass=_unset,
1089 texmessagesbegindoc=_unset,
1090 texmessagesend=_unset,
1091 texmessagesdefaultpreamble=_unset,
1092 texmessagesdefaultrun=_unset):
1093 """provide a set command for TeX/LaTeX settings
1094 - TeX/LaTeX must not yet been started
1095 - especially needed for the defaultrunner, where no access to
1096 the constructor is available"""
1097 if self.texruns:
1098 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1099 if mode is not _unset:
1100 mode = mode.lower()
1101 if mode != "tex" and mode != "latex":
1102 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1103 self.mode = mode
1104 if lfs is not _unset:
1105 self.lfs = lfs
1106 if docclass is not _unset:
1107 self.docclass = docclass
1108 if docopt is not _unset:
1109 self.docopt = docopt
1110 if usefiles is not _unset:
1111 self.usefiles = usefiles
1112 if waitfortex is not _unset:
1113 self.waitfortex = waitfortex
1114 if showwaitfortex is not _unset:
1115 self.showwaitfortex = showwaitfortex
1116 if texipc is not _unset:
1117 self.texipc = texipc
1118 if texdebug is not _unset:
1119 if self.texdebug is not None:
1120 self.texdebug.close()
1121 if texdebug[-4:] == ".tex":
1122 self.texdebug = open(texdebug, "w")
1123 else:
1124 self.texdebug = open("%s.tex" % texdebug, "w")
1125 if dvidebug is not _unset:
1126 self.dvidebug = dvidebug
1127 if errordebug is not _unset:
1128 self.errordebug = errordebug
1129 if pyxgraphics is not _unset:
1130 self.pyxgraphics = pyxgraphics
1131 if errordebug is not _unset:
1132 self.errordebug = errordebug
1133 if texmessagesstart is not _unset:
1134 self.texmessagesstart = texmessagesstart
1135 if texmessagesdocclass is not _unset:
1136 self.texmessagesdocclass = texmessagesdocclass
1137 if texmessagesbegindoc is not _unset:
1138 self.texmessagesbegindoc = texmessagesbegindoc
1139 if texmessagesend is not _unset:
1140 self.texmessagesend = texmessagesend
1141 if texmessagesdefaultpreamble is not _unset:
1142 self.texmessagesdefaultpreamble = texmessagesdefaultpreamble
1143 if texmessagesdefaultrun is not _unset:
1144 self.texmessagesdefaultrun = texmessagesdefaultrun
1146 def preamble(self, expr, texmessages=[]):
1147 r"""put something into the TeX/LaTeX preamble
1148 - in LaTeX, this is done before the \begin{document}
1149 (you might use \AtBeginDocument, when you're in need for)
1150 - it is not allowed to call preamble after calling the
1151 text method for the first time (for LaTeX this is needed
1152 due to \begin{document}; in TeX it is forced for compatibility
1153 (you should be able to switch from TeX to LaTeX, if you want,
1154 without breaking something)
1155 - preamble expressions must not create any dvi output
1156 - args might contain texmessage instances"""
1157 if self.texdone or not self.preamblemode:
1158 raise RuntimeError("preamble calls disabled due to previous text calls")
1159 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmessagesdefaultpreamble + texmessages
1160 self.execute(expr, texmessages)
1161 self.preambles.append((expr, texmessages))
1163 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:")
1165 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1166 """create text by passing expr to TeX/LaTeX
1167 - returns a textbox containing the result from running expr thru TeX/LaTeX
1168 - the box center is set to x, y
1169 - *args may contain attr parameters, namely:
1170 - textattr instances
1171 - texmessage instances
1172 - trafo._trafo instances
1173 - style.fillstyle instances"""
1174 if expr is None:
1175 raise ValueError("None expression is invalid")
1176 if self.texdone:
1177 self.reset(reinit=1)
1178 first = 0
1179 if self.preamblemode:
1180 if self.mode == "latex":
1181 self.execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc)
1182 self.preamblemode = 0
1183 first = 1
1184 textattrs = attr.mergeattrs(textattrs) # perform cleans
1185 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1186 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1187 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1188 textattrs = attr.getattrs(textattrs, [textattr])
1189 # reverse loop over the merged textattrs (last is applied first)
1190 lentextattrs = len(textattrs)
1191 for i in range(lentextattrs):
1192 expr = textattrs[lentextattrs-1-i].apply(expr)
1193 try:
1194 self.execute(expr, self.defaulttexmessagesdefaultrun + self.texmessagesdefaultrun + texmessages)
1195 except TexResultError, e:
1196 warnings.warn("We try to finish the dvi due to an unhandled tex error", PyXTeXWarning)
1197 try:
1198 self.finishdvi(ignoretail=1)
1199 except TexResultError:
1200 pass
1201 raise e
1202 if self.texipc:
1203 if first:
1204 self.dvifile = dvifile.DVIfile("%s.dvi" % self.texfilename, debug=self.dvidebug)
1205 match = self.PyXBoxPattern.search(self.texmessage)
1206 if not match or int(match.group("page")) != self.page:
1207 raise TexResultError("box extents not found", self)
1208 left, right, height, depth = [float(xxx)*72/72.27*unit.x_pt for xxx in match.group("lt", "rt", "ht", "dp")]
1209 box = textbox(x, y, left, right, height, depth, self.finishdvi, fillstyles)
1210 for t in trafos:
1211 box.reltransform(t) # TODO: should trafos really use reltransform???
1212 # this is quite different from what we do elsewhere!!!
1213 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1214 if self.texipc:
1215 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0], fontmap=fontmap, singlecharmode=singlecharmode))
1216 else:
1217 box.fontmap = fontmap
1218 box.singlecharmode = singlecharmode
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