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
32 have_subprocess
= False
34 have_subprocess
= True
36 ###############################################################################
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())
75 self
.description
= description
78 return self
.description
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
)
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
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
):
128 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.%s." % (texrunner
.texfilename
, self
.fileending
), 1)
129 texrunner
.texmessageparsed
= s1
+ s2
130 except (IndexError, ValueError):
132 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.%s." % (os
.curdir
,
134 texrunner
.texfilename
,
136 texrunner
.texmessageparsed
= s1
+ s2
137 except (IndexError, ValueError):
141 class _texmessageinputmarker(texmessage
):
142 """validates the PyXInputMarker"""
144 __implements__
= _Itexmessage
146 def check(self
, texrunner
):
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():]
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
):
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)
192 texrunner.texmessageparsed = (texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]).strip()
194 # check for "(see the transcript
file for additional information
)"
196 s1, s2 = texrunner.texmessageparsed.split("(see the transcript
file for additional information
)", 1)
197 texrunner.texmessageparsed = (s1 + s2).strip()
198 except (IndexError, ValueError):
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)
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():]
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
."
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
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"""
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]:
269 elif inquote and c == quotes[1]:
276 if level > highestlevel:
278 if level <= maxlevel:
282 if level == 0 and highestlevel > 0:
285 def check(self, texrunner):
286 search = self.baselevels(texrunner.texmessageparsed)
288 if search is not None:
289 m = self.pattern.search(search)
291 filename = m.group("filename").replace("\n", "")
293 additional = m.group("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()])
300 res.append(search[:m.end()])
301 search = search[m.end():]
302 m = self.pattern.search(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):
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):
331 class _texmessageignore(_texmessageload):
332 """validates any TeX/LaTeX response
333 - this might be used, when the expression is ok, but no suitable texmessage
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()
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)
381 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
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 ###############################################################################
396 ###############################################################################
398 _textattrspreamble = ""
401 "a textattr defines a apply method, which modifies a (La)TeX expression"
403 class _localattr: pass
405 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
407 \leftskip=0pt plus \PyXFlushHAlign fil%
408 \rightskip=0pt plus 1fil%
409 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
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
):
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
):
483 def apply(self
, expr
):
484 return r
"\phantom{%s}" % expr
487 clearphantom
= attr
.clearclass(_phantom
)
490 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
492 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
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
)
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
):
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
):
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
]
591 raise IndexError("index out of sizelist range")
595 def apply(self
, expr
):
596 return r
"\%s{}%s" % (self
.size
, expr
)
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)
608 size
.clear
= attr
.clearclass(size
)
611 ###############################################################################
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)
631 self
.expectqueue
= expectqueue
632 self
.gotevent
= gotevent
633 self
.gotqueue
= gotqueue
634 self
.quitevent
= quitevent
640 # catch interupted system call errors while reading
643 return self
.pipe
.readline()
645 if e
.errno
!= errno
.EINTR
:
647 read
= _read() # read, what comes in
649 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
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
661 self
.expect
= self
.expectqueue
.get_nowait()
666 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
667 raise RuntimeError("TeX/LaTeX finished unexpectedly")
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"""
685 self
.width
= left
+ right
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
)
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:
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")
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(".")
750 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
753 for file in glob
.glob("%s.*" % texrunner
.texfilename
) + ["%sNotes.bib" % texrunner
.texfilename
]:
758 if texrunner
.texdebug
is not None:
760 texrunner
.texdebug
.close()
761 texrunner
.texdebug
= None
770 """TeX/LaTeX interface
771 - runs TeX/LaTeX expressions instantly
772 - checks TeX/LaTeX response
773 - the instance variable texmessage stores the last TeX
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
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",
795 waitfortex
=config
.getint("text", "waitfortex", 60),
796 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
797 texipc
=config
.getboolean("text", "texipc", 0),
803 texmessagesdocclass
=[],
804 texmessagesbegindoc
=[],
806 texmessagesdefaultpreamble
=[],
807 texmessagesdefaultrun
=[]):
809 if mode
!= "tex" and mode
!= "latex":
810 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
813 self
.docclass
= docclass
815 self
.usefiles
= usefiles
[:]
816 self
.waitfortex
= waitfortex
817 self
.showwaitfortex
= showwaitfortex
819 if texdebug
is not None:
820 if texdebug
[-4:] == ".tex":
821 self
.texdebug
= open(texdebug
, "w")
823 self
.texdebug
= open("%s.tex" % texdebug
, "w")
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
[:]
838 self
.preamblemode
= 1
842 self
.needdvitextboxes
= [] # when texipc-mode off
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
:
858 while waited
< self
.waitfortex
and not hasevent
:
859 if self
.waitfortex
- waited
> self
.showwaitfortex
:
860 event
.wait(self
.showwaitfortex
)
861 waited
+= self
.showwaitfortex
863 event
.wait(self
.waitfortex
- waited
)
864 waited
+= self
.waitfortex
- waited
865 hasevent
= event
.isSet()
867 if waited
< self
.waitfortex
:
868 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
870 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
873 event
.wait(self
.waitfortex
)
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"""
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(".")
895 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
898 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
899 texfile
.write("\\relax%\n")
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
911 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
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
)
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":
954 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
957 lfsname
= "%s.lfs" % self
.lfs
958 for fulllfsname
in [lfsname
,
959 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
961 lfsfile
= open(fulllfsname
, "r")
962 lfsdef
= lfsfile
.read()
968 lfserror
= "File '%s' is not available or not readable. " % lfsname
971 if lfserror
is not None:
972 allfiles
= (glob
.glob("*.lfs") +
973 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
978 lfsnames
.append(os
.path
.basename(f
)[:-4])
983 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
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":
991 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
993 open(pyxdef
, "r").close()
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"
1005 if self
.docopt
is not None:
1006 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
1007 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
1009 self
.execute("\\documentclass{%s}" % self
.docclass
,
1010 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
1011 self
.preamblemode
= oldpreamblemode
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
)
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"
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
1036 self
.texinput
.close() # close the input queue and
1037 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1039 self
.texmessage
= ""
1041 self
.texmessage
+= self
.gotqueue
.get_nowait()
1044 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1045 self
.texmessageparsed
= self
.texmessage
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
:
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
)
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
1067 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1068 dvifilename
= "%s.dvi" % self
.texfilename
1070 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvidebug
)
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
))
1075 if not ignoretail
and self
.dvifile
.readpage(None) is not None:
1076 raise RuntimeError("end of dvifile expected")
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))"
1084 if self
.texdebug
is not None:
1085 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
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
1098 self
.preamblemode
= 1
1100 def set(self
, mode
=_unset
,
1106 showwaitfortex
=_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"""
1123 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1124 if mode
is not _unset
:
1126 if mode
!= "tex" and mode
!= "latex":
1127 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1129 if lfs
is not _unset
:
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")
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"""
1200 raise ValueError("None expression is invalid")
1202 self
.reset(reinit
=1)
1204 if self
.preamblemode
:
1205 if self
.mode
== "latex":
1206 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1207 self
.preamblemode
= 0
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
)
1219 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1220 except TexResultError
:
1221 self
.finishdvi(ignoretail
=1)
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
)
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
1236 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0], fontmap
=fontmap
))
1238 box
.fontmap
= fontmap
1239 self
.needdvitextboxes
.append(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"
1269 self
.execute(pageshapes_str
, [])
1270 parnos_str
= "}{".join(parnos
)
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"
1284 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1286 if self
.dvifile
is None:
1287 self
.dvifile
= dvifile
.DVIfile("%s.dvi" % self
.texfilename
, debug
=self
.dvidebug
)
1289 raise RuntimeError("textboxes currently needs texipc")
1292 lastparshapes
= parshapes
1295 lastpar
= prevgraf
= -1
1296 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
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])
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])
1310 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1313 # a new paragraph is to be broken
1314 parnos
.append(str(par
))
1315 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1317 parshape
= " 0pt " + parshape
1318 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1319 elif prevgraf
== lastprevgraf
:
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]
1326 oldparshape
= " " + oldparshape
1327 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1329 parshape
= " 0pt " + parshape
1332 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1334 lastprevgraf
= prevgraf
1336 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
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
:
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
={" ": "~",
1367 "\\": "{$\setminus$}",
1369 "escape all ascii characters such that they are printable by TeX/LaTeX"
1372 if not 32 <= ord(s
[i
]) < 127:
1373 raise ValueError("escapestring function handles ascii strings only")
1380 s
= s
[:i
] + r
+ s
[i
+1:]