1 # -*- coding: ISO-8859-1 -*-
4 # Copyright (C) 2002-2004 Jörg Lehmann <joergl@users.sourceforge.net>
5 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
6 # Copyright (C) 2002-2006 André Wobst <wobsta@users.sourceforge.net>
8 # This file is part of PyX (http://pyx.sourceforge.net/).
10 # PyX is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
15 # PyX is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with PyX; if not, write to the Free Software
22 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 import errno
, glob
, os
, threading
, Queue
, re
, tempfile
, atexit
, time
, warnings
25 import config
, siteconfig
, unit
, box
, canvas
, trafo
, version
, attr
, style
26 from pyx
.dvi
import dvifile
27 import bbox
as bboxmodule
29 ###############################################################################
31 # - please don't get confused:
32 # - there is a texmessage (and a texmessageparsed) attribute within the
33 # texrunner; it contains TeX/LaTeX response from the last command execution
34 # - instances of classes derived from the class texmessage are used to
35 # parse the TeX/LaTeX response as it is stored in the texmessageparsed
36 # attribute of a texrunner instance
37 # - the multiple usage of the name texmessage might be removed in the future
38 # - texmessage instances should implement _Itexmessage
39 ###############################################################################
41 class TexResultError(RuntimeError):
42 """specialized texrunner exception class
43 - it is raised by texmessage instances, when a texmessage indicates an error
44 - it is raised by the texrunner itself, whenever there is a texmessage left
45 after all parsing of this message (by texmessage instances)
46 prints a detailed report about the problem
47 - the verbose level is controlled by texrunner.errordebug"""
49 def __init__(self
, description
, texrunner
):
50 if texrunner
.errordebug
>= 2:
51 self
.description
= ("%s\n" % description
+
52 "The expression passed to TeX was:\n"
53 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
54 "The return message from TeX was:\n"
55 " %s\n" % texrunner
.texmessage
.replace("\n", "\n ").rstrip() +
56 "After parsing this message, the following was left:\n"
57 " %s" % texrunner
.texmessageparsed
.replace("\n", "\n ").rstrip())
58 elif texrunner
.errordebug
== 1:
59 firstlines
= texrunner
.texmessageparsed
.split("\n")
60 if len(firstlines
) > 5:
61 firstlines
= firstlines
[:5] + ["(cut after 5 lines, increase errordebug for more output)"]
62 self
.description
= ("%s\n" % description
+
63 "The expression passed to TeX was:\n"
64 " %s\n" % texrunner
.expr
.replace("\n", "\n ").rstrip() +
65 "After parsing the return message from TeX, the following was left:\n" +
66 reduce(lambda x
, y
: "%s %s\n" % (x
,y
), firstlines
, "").rstrip())
68 self
.description
= description
71 return self
.description
75 """validates/invalidates TeX/LaTeX response"""
77 def check(self
, texrunner
):
78 """check a Tex/LaTeX response and respond appropriate
79 - read the texrunners texmessageparsed attribute
80 - if there is an problem found, raise TexResultError
81 - remove any valid and identified TeX/LaTeX response
82 from the texrunners texmessageparsed attribute
83 -> finally, there should be nothing left in there,
84 otherwise it is interpreted as an error"""
87 class texmessage(attr
.attr
): pass
90 class _texmessagestart(texmessage
):
91 """validates TeX/LaTeX startup"""
93 __implements__
= _Itexmessage
95 startpattern
= re
.compile(r
"This is [-0-9a-zA-Z\s_]*TeX")
97 def check(self
, texrunner
):
98 # check for "This is e-TeX"
99 m
= self
.startpattern
.search(texrunner
.texmessageparsed
)
101 raise TexResultError("TeX startup failed", texrunner
)
102 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[m
.end():]
104 # check for \raiseerror -- just to be sure that communication works
106 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[1]
107 except (IndexError, ValueError):
108 raise TexResultError("TeX scrollmode check failed", texrunner
)
111 class _texmessagenofile(texmessage
):
112 """allows for LaTeXs no-file warning"""
114 __implements__
= _Itexmessage
116 def __init__(self
, fileending
):
117 self
.fileending
= fileending
119 def check(self
, texrunner
):
121 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s.%s." % (texrunner
.texfilename
, self
.fileending
), 1)
122 texrunner
.texmessageparsed
= s1
+ s2
123 except (IndexError, ValueError):
125 s1
, s2
= texrunner
.texmessageparsed
.split("No file %s%s%s.%s." % (os
.curdir
,
127 texrunner
.texfilename
,
129 texrunner
.texmessageparsed
= s1
+ s2
130 except (IndexError, ValueError):
134 class _texmessageinputmarker(texmessage
):
135 """validates the PyXInputMarker"""
137 __implements__
= _Itexmessage
139 def check(self
, texrunner
):
141 s1
, s2
= texrunner
.texmessageparsed
.split("PyXInputMarker:executeid=%s:" % texrunner
.executeid
, 1)
142 texrunner
.texmessageparsed
= s1
+ s2
143 except (IndexError, ValueError):
144 raise TexResultError("PyXInputMarker expected", texrunner
)
147 class _texmessagepyxbox(texmessage
):
148 """validates the PyXBox output"""
150 __implements__
= _Itexmessage
152 pattern
= re
.compile(r
"PyXBox:page=(?P<page>\d+),lt=-?\d*((\d\.?)|(\.?\d))\d*pt,rt=-?\d*((\d\.?)|(\.?\d))\d*pt,ht=-?\d*((\d\.?)|(\.?\d))\d*pt,dp=-?\d*((\d\.?)|(\.?\d))\d*pt:")
154 def check(self
, texrunner
):
155 m
= self
.pattern
.search(texrunner
.texmessageparsed
)
156 if m
and m
.group("page") == str(texrunner
.page
):
157 texrunner
.texmessageparsed
= texrunner
.texmessageparsed
[:m
.start()] + texrunner
.texmessageparsed
[m
.end():]
159 raise TexResultError("PyXBox expected", texrunner
)
162 class _texmessagepyxpageout(texmessage
):
163 """validates the dvi shipout message (writing a page to the dvi file)"""
165 __implements__
= _Itexmessage
167 def check(self
, texrunner
):
169 s1
, s2
= texrunner
.texmessageparsed
.split("[80.121.88.%s]" % texrunner
.page
, 1)
170 texrunner
.texmessageparsed
= s1
+ s2
171 except (IndexError, ValueError):
172 raise TexResultError("PyXPageOutMarker expected", texrunner
)
175 class _texmessageend(texmessage
):
176 """validates TeX/LaTeX finish"""
178 __implements__
= _Itexmessage
180 auxPattern
= re
.compile(r
"\(([^()]+\.aux|\"[^
\"]+\
.aux
\")\
)")
182 def check(self, texrunner):
183 m = self.auxPattern.search(texrunner.texmessageparsed)
185 texrunner.texmessageparsed = (texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]).strip()
187 # check for "(see the transcript
file for additional information
)"
189 s1, s2 = texrunner.texmessageparsed.split("(see the transcript
file for additional information
)", 1)
190 texrunner.texmessageparsed = (s1 + s2).strip()
191 except (IndexError, ValueError):
194 # check for "Output written on
...dvi (1 page
, 220 bytes
)."
195 dvipattern = re.compile(r"Output written on
%s\
.dvi \
((?P
<page
>\d
+) pages?
, \d
+ bytes\
)\
." % texrunner.texfilename)
196 m = dvipattern.search(texrunner.texmessageparsed)
199 raise TexResultError("TeX dvifile messages expected
", texrunner)
200 if m.group("page
") != str(texrunner.page):
201 raise TexResultError("wrong number of pages reported
", texrunner)
202 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
205 s1, s2 = texrunner.texmessageparsed.split("No pages of output
.", 1)
206 texrunner.texmessageparsed = s1 + s2
207 except (IndexError, ValueError):
208 raise TexResultError("no dvifile expected
", texrunner)
210 # check for "Transcript written on
...log
."
212 s1, s2 = texrunner.texmessageparsed.split("Transcript written on
%s.log
." % texrunner.texfilename, 1)
213 texrunner.texmessageparsed = s1 + s2
214 except (IndexError, ValueError):
215 raise TexResultError("TeX logfile message expected
", texrunner)
218 class _texmessageemptylines(texmessage):
219 """validates "*-only
" (TeX/LaTeX input marker in interactive mode) and empty lines
220 also clear TeX interactive mode warning (Please type a command or say `\\end')
223 __implements__ = _Itexmessage
225 def check(self, texrunner):
226 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(r"(Please
type a command
or say `\end
')", "")
227 texrunner.texmessageparsed = texrunner.texmessageparsed.replace(" ", "")
228 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("*\n", "")
229 texrunner.texmessageparsed = texrunner.texmessageparsed.replace("\n", "")
232 class _texmessageload(texmessage):
233 """validates inclusion of arbitrary files
234 - the matched pattern is "(<filename> <arbitrary other stuff>)", where
235 <filename> is a readable file and other stuff can be anything
236 - If the filename is enclosed in double quotes, it may contain blank space.
237 - "(" and ")" must be used consistent (otherwise this validator just does nothing)
238 - this is not always wanted, but we just assume that file inclusion is fine"""
240 __implements__ = _Itexmessage
242 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?<!\")[^()\s\n]+(?!\"))|[^\"\n]+)[\"]?(?P<additional>[^()]*)\)")
244 def baselevels(self, s, maxlevel=1, brackets="()", quotes='""'):
245 """strip parts of a string above a given bracket level
246 - return a modified (some parts might be removed) version of the string s
247 where all parts inside brackets with level higher than maxlevel are
249 - if brackets do not match (number of left and right brackets is wrong
250 or at some points there were more right brackets than left brackets)
251 just return the unmodified string
252 - a quoted string immediately followed after a bracket is left untouched
253 even if it contains quotes itself"""
258 for i, c in enumerate(s):
259 if quotes and level <= maxlevel:
260 if not inquote and c == quotes[0] and i and s[i-1] == brackets[0]:
262 elif inquote and c == quotes[1]:
269 if level > highestlevel:
271 if level <= maxlevel:
275 if level == 0 and highestlevel > 0:
278 def check(self, texrunner):
279 search = self.baselevels(texrunner.texmessageparsed)
281 if search is not None:
282 m = self.pattern.search(search)
284 filename = m.group("filename").replace("\n", "")
286 additional = m.group("additional")
289 if (os.access(filename, os.R_OK) or
290 len(additional) and additional[0] == "\n" and os.access(filename+additional.split()[0], os.R_OK)):
291 res.append(search[:m.start()])
293 res.append(search[:m.end()])
294 search = search[m.end():]
295 m = self.pattern.search(search)
298 texrunner.texmessageparsed = "".join(res)
301 class _texmessageloaddef(_texmessageload):
302 """validates the inclusion of font description files (fd-files)
303 - works like _texmessageload
304 - filename must end with .def or .fd and no further text is allowed"""
305 - further text is allowed"""
307 pattern = re.compile(r"\([\"]?(?P<filename>(?:(?:(?<!\")[^\(\)\s\n\"]+)|(?:(?<=\")[^\(\)\"]+))(\.fd|\.def))[\"]?[\s\n]*(?P<additional>[\(]?[^\(\)]*[\)]?)[\s\n]*\)")
309 def baselevels(self, s, **kwargs):
313 class _texmessagegraphicsload(_texmessageload):
314 """validates the inclusion of files as the graphics packages writes it
315 - works like _texmessageload, but using "<" and ">" as delimiters
316 - filename must end with .eps and no further text is allowed"""
318 pattern = re.compile(r"<(?P<filename>[^>]+.eps)>")
320 def baselevels(self, s, **kwargs):
324 class _texmessageignore(_texmessageload):
325 """validates any TeX/LaTeX response
326 - this might be used, when the expression is ok, but no suitable texmessage
328 - PLEASE: - consider writing suitable tex message parsers
329 - share your ideas/problems/solutions with others (use the PyX mailing lists)"""
331 __implements__ = _Itexmessage
333 def check(self, texrunner):
334 texrunner.texmessageparsed = ""
337 texmessage.start = _texmessagestart()
338 texmessage.noaux = _texmessagenofile("aux")
339 texmessage.nonav = _texmessagenofile("nav")
340 texmessage.end = _texmessageend()
341 texmessage.load = _texmessageload()
342 texmessage.loaddef = _texmessageloaddef()
343 texmessage.graphicsload = _texmessagegraphicsload()
344 texmessage.ignore = _texmessageignore()
347 texmessage.inputmarker = _texmessageinputmarker()
348 texmessage.pyxbox = _texmessagepyxbox()
349 texmessage.pyxpageout = _texmessagepyxpageout()
350 texmessage.emptylines = _texmessageemptylines()
353 class _texmessageallwarning(texmessage):
354 """validates a given pattern 'pattern
' as a warning 'warning
'"""
356 def check(self, texrunner):
357 if texrunner.texmessageparsed:
358 warnings.warn("ignoring all warnings:\n%s" % texrunner.texmessageparsed)
359 texrunner.texmessageparsed = ""
361 texmessage.allwarning = _texmessageallwarning()
364 class texmessagepattern(texmessage):
365 """validates a given pattern and issue a warning (when set)"""
367 def __init__(self, pattern, warning=None):
368 self.pattern = pattern
369 self.warning = warning
371 def check(self, texrunner):
372 m = self.pattern.search(texrunner.texmessageparsed)
374 texrunner.texmessageparsed = texrunner.texmessageparsed[:m.start()] + texrunner.texmessageparsed[m.end():]
376 warnings.warn("%s:\n%s" % (self.warning, m.string[m.start(): m.end()].rstrip()))
377 m = self.pattern.search(texrunner.texmessageparsed)
379 texmessage.fontwarning = texmessagepattern(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "ignoring font warning")
380 texmessage.boxwarning = texmessagepattern(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "ignoring overfull/underfull box warning")
381 texmessage.rerunwarning = texmessagepattern(re.compile(r"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re.MULTILINE), "ignoring rerun warning")
382 texmessage.packagewarning = texmessagepattern(re.compile(r"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re.MULTILINE | re.IGNORECASE), "ignoring generic package warning")
383 texmessage.nobblwarning = texmessagepattern(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE), "ignoring no-bbl warning")
387 ###############################################################################
389 ###############################################################################
391 _textattrspreamble = ""
394 "a textattr defines a apply method, which modifies a (La)TeX expression"
396 class _localattr: pass
398 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
400 \leftskip=0pt plus \PyXFlushHAlign fil%
401 \rightskip=0pt plus 1fil%
402 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
408 \exhyphenpenalty=9999}%
411 class boxhalign(attr.exclusiveattr, textattr, _localattr):
413 def __init__(self, aboxhalign):
414 self.boxhalign = aboxhalign
415 attr.exclusiveattr.__init__(self, boxhalign)
417 def apply(self, expr):
418 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
420 boxhalign.left = boxhalign(0)
421 boxhalign.center = boxhalign(0.5)
422 boxhalign.right = boxhalign(1)
423 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass
for boxhalign since it can
't clear a halign's boxhalign
426 class flushhalign(attr
.exclusiveattr
, textattr
, _localattr
):
428 def __init__(self
, aflushhalign
):
429 self
.flushhalign
= aflushhalign
430 attr
.exclusiveattr
.__init
__(self
, flushhalign
)
432 def apply(self
, expr
):
433 return r
"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.flushhalign
, expr
)
435 flushhalign
.left
= flushhalign(0)
436 flushhalign
.center
= flushhalign(0.5)
437 flushhalign
.right
= flushhalign(1)
438 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
441 class halign(attr
.exclusiveattr
, textattr
, boxhalign
, flushhalign
, _localattr
):
443 def __init__(self
, aboxhalign
, aflushhalign
):
444 self
.boxhalign
= aboxhalign
445 self
.flushhalign
= aflushhalign
446 attr
.exclusiveattr
.__init
__(self
, halign
)
448 def apply(self
, expr
):
449 return r
"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self
.boxhalign
, self
.flushhalign
, expr
)
451 halign
.left
= halign(0, 0)
452 halign
.center
= halign(0.5, 0.5)
453 halign
.right
= halign(1, 1)
454 halign
.clear
= attr
.clearclass(halign
)
455 halign
.boxleft
= boxhalign
.left
456 halign
.boxcenter
= boxhalign
.center
457 halign
.boxright
= boxhalign
.right
458 halign
.flushleft
= halign
.raggedright
= flushhalign
.left
459 halign
.flushcenter
= halign
.raggedcenter
= flushhalign
.center
460 halign
.flushright
= halign
.raggedleft
= flushhalign
.right
463 class _mathmode(attr
.attr
, textattr
, _localattr
):
466 def apply(self
, expr
):
467 return r
"$\displaystyle{%s}$" % expr
469 mathmode
= _mathmode()
470 clearmathmode
= attr
.clearclass(_mathmode
)
473 class _phantom(attr
.attr
, textattr
, _localattr
):
476 def apply(self
, expr
):
477 return r
"\phantom{%s}" % expr
480 clearphantom
= attr
.clearclass(_phantom
)
483 _textattrspreamble
+= "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
485 class parbox_pt(attr
.sortbeforeexclusiveattr
, textattr
):
491 def __init__(self
, width
, baseline
=top
):
492 self
.width
= width
* 72.27 / (unit
.scale
["x"] * 72)
493 self
.baseline
= baseline
494 attr
.sortbeforeexclusiveattr
.__init
__(self
, parbox_pt
, [_localattr
])
496 def apply(self
, expr
):
497 if self
.baseline
== self
.top
:
498 return r
"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
499 elif self
.baseline
== self
.middle
:
500 return r
"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self
.width
, expr
, expr
)
501 elif self
.baseline
== self
.bottom
:
502 return r
"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self
.width
, expr
)
504 RuntimeError("invalid baseline argument")
506 parbox_pt
.clear
= attr
.clearclass(parbox_pt
)
508 class parbox(parbox_pt
):
510 def __init__(self
, width
, **kwargs
):
511 parbox_pt
.__init
__(self
, unit
.topt(width
), **kwargs
)
513 parbox
.clear
= parbox_pt
.clear
516 _textattrspreamble
+= "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
518 class valign(attr
.sortbeforeexclusiveattr
, textattr
):
520 def __init__(self
, avalign
):
521 self
.valign
= avalign
522 attr
.sortbeforeexclusiveattr
.__init
__(self
, valign
, [parbox_pt
, _localattr
])
524 def apply(self
, expr
):
525 return r
"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr
, 1-self
.valign
, self
.valign
)
527 valign
.top
= valign(0)
528 valign
.middle
= valign(0.5)
529 valign
.bottom
= valign(1)
530 valign
.clear
= valign
.baseline
= attr
.clearclass(valign
)
533 _textattrspreamble
+= "\\newdimen\\PyXDimenVShift%\n"
535 class _vshift(attr
.sortbeforeattr
, textattr
):
538 attr
.sortbeforeattr
.__init
__(self
, [valign
, parbox_pt
, _localattr
])
540 def apply(self
, expr
):
541 return r
"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self
.setheightexpr(), expr
)
543 class vshift(_vshift
):
544 "vertical down shift by a fraction of a character height"
546 def __init__(self
, lowerratio
, heightstr
="0"):
547 _vshift
.__init
__(self
)
548 self
.lowerratio
= lowerratio
549 self
.heightstr
= heightstr
551 def setheightexpr(self
):
552 return r
"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self
.heightstr
, self
.lowerratio
)
554 class _vshiftmathaxis(_vshift
):
555 "vertical down shift by the height of the math axis"
557 def setheightexpr(self
):
558 return r
"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
561 vshift
.bottomzero
= vshift(0)
562 vshift
.middlezero
= vshift(0.5)
563 vshift
.topzero
= vshift(1)
564 vshift
.mathaxis
= _vshiftmathaxis()
565 vshift
.clear
= attr
.clearclass(_vshift
)
568 defaultsizelist
= ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
569 None, "tiny", "scriptsize", "footnotesize", "small"]
571 class size(attr
.sortbeforeattr
, textattr
):
574 def __init__(self
, sizeindex
=None, sizename
=None, sizelist
=defaultsizelist
):
575 if (sizeindex
is None and sizename
is None) or (sizeindex
is not None and sizename
is not None):
576 raise RuntimeError("either specify sizeindex or sizename")
577 attr
.sortbeforeattr
.__init
__(self
, [_mathmode
, _vshift
])
578 if sizeindex
is not None:
579 if sizeindex
>= 0 and sizeindex
< sizelist
.index(None):
580 self
.size
= sizelist
[sizeindex
]
581 elif sizeindex
< 0 and sizeindex
+ len(sizelist
) > sizelist
.index(None):
582 self
.size
= sizelist
[sizeindex
]
584 raise IndexError("index out of sizelist range")
588 def apply(self
, expr
):
589 return r
"\%s{}%s" % (self
.size
, expr
)
592 size
.scriptsize
= size
.script
= size(-3)
593 size
.footnotesize
= size
.footnote
= size(-2)
594 size
.small
= size(-1)
595 size
.normalsize
= size
.normal
= size(0)
601 size
.clear
= attr
.clearclass(size
)
604 ###############################################################################
606 ###############################################################################
609 class _readpipe(threading
.Thread
):
610 """threaded reader of TeX/LaTeX output
611 - sets an event, when a specific string in the programs output is found
612 - sets an event, when the terminal ends"""
614 def __init__(self
, pipe
, expectqueue
, gotevent
, gotqueue
, quitevent
):
615 """initialize the reader
616 - pipe: file to be read from
617 - expectqueue: keeps the next InputMarker to be wait for
618 - gotevent: the "got InputMarker" event
619 - gotqueue: a queue containing the lines recieved from TeX/LaTeX
620 - quitevent: the "end of terminal" event"""
621 threading
.Thread
.__init
__(self
)
622 self
.setDaemon(1) # don't care if the output might not be finished (nevertheless, it shouldn't happen)
624 self
.expectqueue
= expectqueue
625 self
.gotevent
= gotevent
626 self
.gotqueue
= gotqueue
627 self
.quitevent
= quitevent
633 # catch interupted system call errors while reading
636 return self
.pipe
.readline()
638 if e
.errno
!= errno
.EINTR
:
640 read
= _read() # read, what comes in
642 self
.expect
= self
.expectqueue
.get_nowait() # read, what should be expected
646 # universal EOL handling (convert everything into unix like EOLs)
647 # XXX is this necessary on pipes?
648 read
= read
.replace("\r", "").replace("\n", "") + "\n"
649 self
.gotqueue
.put(read
) # report, whats read
650 if self
.expect
is not None and read
.find(self
.expect
) != -1:
651 self
.gotevent
.set() # raise the got event, when the output was expected (XXX: within a single line)
652 read
= _read() # read again
654 self
.expect
= self
.expectqueue
.get_nowait()
659 if self
.expect
is not None and self
.expect
.find("PyXInputMarker") != -1:
660 raise RuntimeError("TeX/LaTeX finished unexpectedly")
664 class textbox(box
.rect
, canvas
._canvas
):
665 """basically a box.rect, but it contains a text created by the texrunner
666 - texrunner._text and texrunner.text return such an object
667 - _textbox instances can be inserted into a canvas
668 - the output is contained in a page of the dvifile available thru the texrunner"""
669 # TODO: shouldn't all boxes become canvases? how about inserts then?
671 def __init__(self
, x
, y
, left
, right
, height
, depth
, finishdvi
, attrs
):
673 - finishdvi is a method to be called to get the dvicanvas
674 (e.g. the finishdvi calls the setdvicanvas method)
675 - attrs are fillstyles"""
678 self
.width
= left
+ right
681 self
.texttrafo
= trafo
.scale(unit
.scale
["x"]).translated(x
, y
)
682 box
.rect
.__init
__(self
, x
- left
, y
- depth
, left
+ right
, depth
+ height
, abscenter
= (left
, depth
))
683 canvas
._canvas
.__init
__(self
, attrs
)
684 self
.finishdvi
= finishdvi
685 self
.dvicanvas
= None
686 self
.insertdvicanvas
= 0
688 def transform(self
, *trafos
):
689 if self
.insertdvicanvas
:
690 raise RuntimeError("can't apply transformation after dvicanvas was inserted")
691 box
.rect
.transform(self
, *trafos
)
693 self
.texttrafo
= trafo
* self
.texttrafo
695 def setdvicanvas(self
, dvicanvas
):
696 if self
.dvicanvas
is not None:
697 raise RuntimeError("multiple call to setdvicanvas")
698 self
.dvicanvas
= dvicanvas
700 def ensuredvicanvas(self
):
701 if self
.dvicanvas
is None:
703 assert self
.dvicanvas
is not None, "finishdvi is broken"
704 if not self
.insertdvicanvas
:
705 self
.insert(self
.dvicanvas
, [self
.texttrafo
])
706 self
.insertdvicanvas
= 1
708 def marker(self
, marker
):
709 self
.ensuredvicanvas()
710 return self
.texttrafo
.apply(*self
.dvicanvas
.markers
[marker
])
712 def processPS(self
, file, writer
, context
, registry
, bbox
):
713 self
.ensuredvicanvas()
714 abbox
= bboxmodule
.empty()
715 canvas
._canvas
.processPS(self
, file, writer
, context
, registry
, abbox
)
716 bbox
+= box
.rect
.bbox(self
)
718 def processPDF(self
, file, writer
, context
, registry
, bbox
):
719 self
.ensuredvicanvas()
720 abbox
= bboxmodule
.empty()
721 canvas
._canvas
.processPDF(self
, file, writer
, context
, registry
, abbox
)
722 bbox
+= box
.rect
.bbox(self
)
725 def _cleantmp(texrunner
):
726 """get rid of temporary files
727 - function to be registered by atexit
728 - files contained in usefiles are kept"""
729 if texrunner
.texruns
: # cleanup while TeX is still running?
730 texrunner
.expectqueue
.put_nowait(None) # do not expect any output anymore
731 if texrunner
.mode
== "latex": # try to immediately quit from TeX or LaTeX
732 texrunner
.texinput
.write("\n\\catcode`\\@11\\relax\\@@end\n")
734 texrunner
.texinput
.write("\n\\end\n")
735 texrunner
.texinput
.close() # close the input queue and
736 if not texrunner
.waitforevent(texrunner
.quitevent
): # wait for finish of the output
737 return # didn't got a quit from TeX -> we can't do much more
738 texrunner
.texruns
= 0
739 texrunner
.texdone
= 1
740 for usefile
in texrunner
.usefiles
:
741 extpos
= usefile
.rfind(".")
743 os
.rename(texrunner
.texfilename
+ usefile
[extpos
:], usefile
)
746 for file in glob
.glob("%s.*" % texrunner
.texfilename
) + ["%sNotes.bib" % texrunner
.texfilename
]:
751 if texrunner
.texdebug
is not None:
753 texrunner
.texdebug
.close()
754 texrunner
.texdebug
= None
763 """TeX/LaTeX interface
764 - runs TeX/LaTeX expressions instantly
765 - checks TeX/LaTeX response
766 - the instance variable texmessage stores the last TeX
768 - the instance variable texmessageparsed stores a parsed
769 version of texmessage; it should be empty after
770 texmessage.check was called, otherwise a TexResultError
772 - the instance variable errordebug controls the verbose
773 level of TexResultError"""
775 defaulttexmessagesstart
= [texmessage
.start
]
776 defaulttexmessagesdocclass
= [texmessage
.load
]
777 defaulttexmessagesbegindoc
= [texmessage
.load
, texmessage
.noaux
]
778 defaulttexmessagesend
= [texmessage
.end
, texmessage
.fontwarning
, texmessage
.rerunwarning
, texmessage
.nobblwarning
]
779 defaulttexmessagesdefaultpreamble
= [texmessage
.load
]
780 defaulttexmessagesdefaultrun
= [texmessage
.loaddef
, texmessage
.graphicsload
,
781 texmessage
.fontwarning
, texmessage
.boxwarning
, texmessage
.packagewarning
]
783 def __init__(self
, mode
="tex",
788 waitfortex
=config
.getint("text", "waitfortex", 60),
789 showwaitfortex
=config
.getint("text", "showwaitfortex", 5),
790 texipc
=config
.getboolean("text", "texipc", 0),
796 texmessagesdocclass
=[],
797 texmessagesbegindoc
=[],
799 texmessagesdefaultpreamble
=[],
800 texmessagesdefaultrun
=[]):
802 if mode
!= "tex" and mode
!= "latex":
803 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
806 self
.docclass
= docclass
808 self
.usefiles
= usefiles
[:]
809 self
.waitfortex
= waitfortex
810 self
.showwaitfortex
= showwaitfortex
812 if texdebug
is not None:
813 if texdebug
[-4:] == ".tex":
814 self
.texdebug
= open(texdebug
, "w")
816 self
.texdebug
= open("%s.tex" % texdebug
, "w")
819 self
.dvidebug
= dvidebug
820 self
.errordebug
= errordebug
821 self
.pyxgraphics
= pyxgraphics
822 self
.texmessagesstart
= texmessagesstart
[:]
823 self
.texmessagesdocclass
= texmessagesdocclass
[:]
824 self
.texmessagesbegindoc
= texmessagesbegindoc
[:]
825 self
.texmessagesend
= texmessagesend
[:]
826 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
[:]
827 self
.texmessagesdefaultrun
= texmessagesdefaultrun
[:]
831 self
.preamblemode
= 1
835 self
.needdvitextboxes
= [] # when texipc-mode off
837 self
.textboxesincluded
= 0
838 savetempdir
= tempfile
.tempdir
839 tempfile
.tempdir
= os
.curdir
840 self
.texfilename
= os
.path
.basename(tempfile
.mktemp())
841 tempfile
.tempdir
= savetempdir
843 def waitforevent(self
, event
):
844 """waits verbosely with an timeout for an event
845 - observes an event while periodly while printing messages
846 - returns the status of the event (isSet)
847 - does not clear the event"""
848 if self
.showwaitfortex
:
851 while waited
< self
.waitfortex
and not hasevent
:
852 if self
.waitfortex
- waited
> self
.showwaitfortex
:
853 event
.wait(self
.showwaitfortex
)
854 waited
+= self
.showwaitfortex
856 event
.wait(self
.waitfortex
- waited
)
857 waited
+= self
.waitfortex
- waited
858 hasevent
= event
.isSet()
860 if waited
< self
.waitfortex
:
861 warnings
.warn("still waiting for %s after %i (of %i) seconds..." % (self
.mode
, waited
, self
.waitfortex
))
863 warnings
.warn("the timeout of %i seconds expired and %s did not respond." % (waited
, self
.mode
))
866 event
.wait(self
.waitfortex
)
869 def execute(self
, expr
, texmessages
):
870 """executes expr within TeX/LaTeX
871 - if self.texruns is not yet set, TeX/LaTeX is initialized,
872 self.texruns is set and self.preamblemode is set
873 - the method must not be called, when self.texdone is already set
874 - expr should be a string or None
875 - when expr is None, TeX/LaTeX is stopped, self.texruns is unset and
876 self.texdone becomes set
877 - when self.preamblemode is set, the expr is passed directly to TeX/LaTeX
878 - when self.preamblemode is unset, the expr is passed to \ProcessPyXBox
879 - texmessages is a list of texmessage instances"""
881 if self
.texdebug
is not None:
882 self
.texdebug
.write("%% PyX %s texdebug file\n" % version
.version
)
883 self
.texdebug
.write("%% mode: %s\n" % self
.mode
)
884 self
.texdebug
.write("%% date: %s\n" % time
.asctime(time
.localtime(time
.time())))
885 for usefile
in self
.usefiles
:
886 extpos
= usefile
.rfind(".")
888 os
.rename(usefile
, self
.texfilename
+ usefile
[extpos
:])
891 texfile
= open("%s.tex" % self
.texfilename
, "w") # start with filename -> creates dvi file with that name
892 texfile
.write("\\relax%\n")
899 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t", 0)
901 # XXX: workaround for MS Windows (bufsize = 0 makes trouble!?)
902 self
.texinput
, self
.texoutput
= os
.popen4("%s%s %s" % (self
.mode
, ipcflag
, self
.texfilename
), "t")
903 atexit
.register(_cleantmp
, self
)
904 self
.expectqueue
= Queue
.Queue(1) # allow for a single entry only -> keeps the next InputMarker to be wait for
905 self
.gotevent
= threading
.Event() # keeps the got inputmarker event
906 self
.gotqueue
= Queue
.Queue(0) # allow arbitrary number of entries
907 self
.quitevent
= threading
.Event() # keeps for end of terminal event
908 self
.readoutput
= _readpipe(self
.texoutput
, self
.expectqueue
, self
.gotevent
, self
.gotqueue
, self
.quitevent
)
910 oldpreamblemode
= self
.preamblemode
911 self
.preamblemode
= 1
912 self
.readoutput
.start()
913 self
.execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
914 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
915 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
916 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
917 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
918 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
919 "\\newdimen\\PyXDimenHAlignRT%\n" +
920 _textattrspreamble
+ # insert preambles for textattrs macros
921 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
922 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
923 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
924 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
925 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
926 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
927 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
928 "lt=\\the\\PyXDimenHAlignLT,"
929 "rt=\\the\\PyXDimenHAlignRT,"
930 "ht=\\the\\ht\\PyXBox,"
931 "dp=\\the\\dp\\PyXBox:}%\n"
932 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
933 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
934 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
935 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
936 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
937 self
.defaulttexmessagesstart
+ self
.texmessagesstart
)
938 os
.remove("%s.tex" % self
.texfilename
)
939 if self
.mode
== "tex":
942 if len(self
.lfs
) > 4 and self
.lfs
[-4:] == ".lfs":
945 lfsname
= "%s.lfs" % self
.lfs
946 for fulllfsname
in [lfsname
,
947 os
.path
.join(siteconfig
.lfsdir
, lfsname
)]:
949 lfsfile
= open(fulllfsname
, "r")
950 lfsdef
= lfsfile
.read()
956 lfserror
= "File '%s' is not available or not readable. " % lfsname
959 if lfserror
is not None:
960 allfiles
= (glob
.glob("*.lfs") +
961 glob
.glob(os
.path
.join(siteconfig
.lfsdir
, "*.lfs")))
966 lfsnames
.append(os
.path
.basename(f
)[:-4])
971 raise IOError("%sAvailable LaTeX font size files (*.lfs): %s" % (lfserror
, lfsnames
))
973 raise IOError("%sNo LaTeX font size files (*.lfs) available. Check your installation." % lfserror
)
974 self
.execute(lfsdef
, [])
975 self
.execute("\\normalsize%\n", [])
976 self
.execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [])
977 elif self
.mode
== "latex":
979 pyxdef
= os
.path
.join(siteconfig
.sharedir
, "pyx.def")
981 open(pyxdef
, "r").close()
983 IOError("file 'pyx.def' is not available or not readable. Check your installation or turn off the pyxgraphics option.")
984 pyxdef
= os
.path
.abspath(pyxdef
).replace(os
.sep
, "/")
985 self
.execute("\\makeatletter%\n"
986 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
987 "\\def\\ProcessOptions{%\n"
988 "\\def\\Gin@driver{" + pyxdef
+ "}%\n"
989 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
990 "\\saveProcessOptions}%\n"
993 if self
.docopt
is not None:
994 self
.execute("\\documentclass[%s]{%s}" % (self
.docopt
, self
.docclass
),
995 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
997 self
.execute("\\documentclass{%s}" % self
.docclass
,
998 self
.defaulttexmessagesdocclass
+ self
.texmessagesdocclass
)
999 self
.preamblemode
= oldpreamblemode
1001 if expr
is not None: # TeX/LaTeX should process expr
1002 self
.expectqueue
.put_nowait("PyXInputMarker:executeid=%i:" % self
.executeid
)
1003 if self
.preamblemode
:
1004 self
.expr
= ("%s%%\n" % expr
+
1005 "\\PyXInput{%i}%%\n" % self
.executeid
)
1008 self
.expr
= ("\\ProcessPyXBox{%s%%\n}{%i}%%\n" % (expr
, self
.page
) +
1009 "\\PyXInput{%i}%%\n" % self
.executeid
)
1010 else: # TeX/LaTeX should be finished
1011 self
.expectqueue
.put_nowait("Transcript written on %s.log" % self
.texfilename
)
1012 if self
.mode
== "latex":
1013 self
.expr
= "\\end{document}%\n"
1015 self
.expr
= "\\end%\n"
1016 if self
.texdebug
is not None:
1017 self
.texdebug
.write(self
.expr
)
1018 self
.texinput
.write(self
.expr
)
1019 gotevent
= self
.waitforevent(self
.gotevent
)
1020 self
.gotevent
.clear()
1021 if expr
is None and gotevent
: # TeX/LaTeX should have finished
1024 self
.texinput
.close() # close the input queue and
1025 gotevent
= self
.waitforevent(self
.quitevent
) # wait for finish of the output
1027 self
.texmessage
= ""
1029 self
.texmessage
+= self
.gotqueue
.get_nowait()
1032 self
.texmessage
= self
.texmessage
.replace("\r\n", "\n").replace("\r", "\n")
1033 self
.texmessageparsed
= self
.texmessage
1035 if expr
is not None:
1036 texmessage
.inputmarker
.check(self
)
1037 if not self
.preamblemode
:
1038 texmessage
.pyxbox
.check(self
)
1039 texmessage
.pyxpageout
.check(self
)
1040 texmessages
= attr
.mergeattrs(texmessages
)
1041 for t
in texmessages
:
1043 keeptexmessageparsed
= self
.texmessageparsed
1044 texmessage
.emptylines
.check(self
)
1045 if len(self
.texmessageparsed
):
1046 self
.texmessageparsed
= keeptexmessageparsed
1047 raise TexResultError("unhandled TeX response (might be an error)", self
)
1049 raise TexResultError("TeX didn't respond as expected within the timeout period (%i seconds)." % self
.waitfortex
, self
)
1051 def finishdvi(self
, ignoretail
=0):
1052 """finish TeX/LaTeX and read the dvifile
1053 - this method ensures that all textboxes can access their
1055 self
.execute(None, self
.defaulttexmessagesend
+ self
.texmessagesend
)
1056 dvifilename
= "%s.dvi" % self
.texfilename
1058 self
.dvifile
= dvifile
.DVIfile(dvifilename
, debug
=self
.dvidebug
)
1060 for box
in self
.needdvitextboxes
:
1061 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), page
, 0, 0, 0, 0, 0, 0], fontmap
=box
.fontmap
))
1063 if not ignoretail
and self
.dvifile
.readpage(None) is not None:
1064 raise RuntimeError("end of dvifile expected")
1066 self
.needdvitextboxes
= []
1068 def reset(self
, reinit
=0):
1069 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1072 if self
.texdebug
is not None:
1073 self
.texdebug
.write("%s\n%% preparing restart of %s\n" % ("%"*80, self
.mode
))
1078 self
.preamblemode
= 1
1079 for expr
, texmessages
in self
.preambles
:
1080 self
.execute(expr
, texmessages
)
1081 if self
.mode
== "latex":
1082 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1083 self
.preamblemode
= 0
1086 self
.preamblemode
= 1
1088 def set(self
, mode
=_unset
,
1094 showwaitfortex
=_unset
,
1100 texmessagesstart
=_unset
,
1101 texmessagesdocclass
=_unset
,
1102 texmessagesbegindoc
=_unset
,
1103 texmessagesend
=_unset
,
1104 texmessagesdefaultpreamble
=_unset
,
1105 texmessagesdefaultrun
=_unset
):
1106 """provide a set command for TeX/LaTeX settings
1107 - TeX/LaTeX must not yet been started
1108 - especially needed for the defaultrunner, where no access to
1109 the constructor is available"""
1111 raise RuntimeError("set not allowed -- TeX/LaTeX already started")
1112 if mode
is not _unset
:
1114 if mode
!= "tex" and mode
!= "latex":
1115 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1117 if lfs
is not _unset
:
1119 if docclass
is not _unset
:
1120 self
.docclass
= docclass
1121 if docopt
is not _unset
:
1122 self
.docopt
= docopt
1123 if usefiles
is not _unset
:
1124 self
.usefiles
= usefiles
1125 if waitfortex
is not _unset
:
1126 self
.waitfortex
= waitfortex
1127 if showwaitfortex
is not _unset
:
1128 self
.showwaitfortex
= showwaitfortex
1129 if texipc
is not _unset
:
1130 self
.texipc
= texipc
1131 if texdebug
is not _unset
:
1132 if self
.texdebug
is not None:
1133 self
.texdebug
.close()
1134 if texdebug
[-4:] == ".tex":
1135 self
.texdebug
= open(texdebug
, "w")
1137 self
.texdebug
= open("%s.tex" % texdebug
, "w")
1138 if dvidebug
is not _unset
:
1139 self
.dvidebug
= dvidebug
1140 if errordebug
is not _unset
:
1141 self
.errordebug
= errordebug
1142 if pyxgraphics
is not _unset
:
1143 self
.pyxgraphics
= pyxgraphics
1144 if errordebug
is not _unset
:
1145 self
.errordebug
= errordebug
1146 if texmessagesstart
is not _unset
:
1147 self
.texmessagesstart
= texmessagesstart
1148 if texmessagesdocclass
is not _unset
:
1149 self
.texmessagesdocclass
= texmessagesdocclass
1150 if texmessagesbegindoc
is not _unset
:
1151 self
.texmessagesbegindoc
= texmessagesbegindoc
1152 if texmessagesend
is not _unset
:
1153 self
.texmessagesend
= texmessagesend
1154 if texmessagesdefaultpreamble
is not _unset
:
1155 self
.texmessagesdefaultpreamble
= texmessagesdefaultpreamble
1156 if texmessagesdefaultrun
is not _unset
:
1157 self
.texmessagesdefaultrun
= texmessagesdefaultrun
1159 def preamble(self
, expr
, texmessages
=[]):
1160 r
"""put something into the TeX/LaTeX preamble
1161 - in LaTeX, this is done before the \begin{document}
1162 (you might use \AtBeginDocument, when you're in need for)
1163 - it is not allowed to call preamble after calling the
1164 text method for the first time (for LaTeX this is needed
1165 due to \begin{document}; in TeX it is forced for compatibility
1166 (you should be able to switch from TeX to LaTeX, if you want,
1167 without breaking something)
1168 - preamble expressions must not create any dvi output
1169 - args might contain texmessage instances"""
1170 if self
.texdone
or not self
.preamblemode
:
1171 raise RuntimeError("preamble calls disabled due to previous text calls")
1172 texmessages
= self
.defaulttexmessagesdefaultpreamble
+ self
.texmessagesdefaultpreamble
+ texmessages
1173 self
.execute(expr
, texmessages
)
1174 self
.preambles
.append((expr
, texmessages
))
1176 PyXBoxPattern
= re
.compile(r
"PyXBox:page=(?P<page>\d+),lt=(?P<lt>-?\d*((\d\.?)|(\.?\d))\d*)pt,rt=(?P<rt>-?\d*((\d\.?)|(\.?\d))\d*)pt,ht=(?P<ht>-?\d*((\d\.?)|(\.?\d))\d*)pt,dp=(?P<dp>-?\d*((\d\.?)|(\.?\d))\d*)pt:")
1178 def text(self
, x
, y
, expr
, textattrs
=[], texmessages
=[], fontmap
=None):
1179 """create text by passing expr to TeX/LaTeX
1180 - returns a textbox containing the result from running expr thru TeX/LaTeX
1181 - the box center is set to x, y
1182 - *args may contain attr parameters, namely:
1183 - textattr instances
1184 - texmessage instances
1185 - trafo._trafo instances
1186 - style.fillstyle instances"""
1188 raise ValueError("None expression is invalid")
1190 self
.reset(reinit
=1)
1192 if self
.preamblemode
:
1193 if self
.mode
== "latex":
1194 self
.execute("\\begin{document}", self
.defaulttexmessagesbegindoc
+ self
.texmessagesbegindoc
)
1195 self
.preamblemode
= 0
1197 textattrs
= attr
.mergeattrs(textattrs
) # perform cleans
1198 attr
.checkattrs(textattrs
, [textattr
, trafo
.trafo_pt
, style
.fillstyle
])
1199 trafos
= attr
.getattrs(textattrs
, [trafo
.trafo_pt
])
1200 fillstyles
= attr
.getattrs(textattrs
, [style
.fillstyle
])
1201 textattrs
= attr
.getattrs(textattrs
, [textattr
])
1202 # reverse loop over the merged textattrs (last is applied first)
1203 lentextattrs
= len(textattrs
)
1204 for i
in range(lentextattrs
):
1205 expr
= textattrs
[lentextattrs
-1-i
].apply(expr
)
1207 self
.execute(expr
, self
.defaulttexmessagesdefaultrun
+ self
.texmessagesdefaultrun
+ texmessages
)
1208 except TexResultError
:
1209 self
.finishdvi(ignoretail
=1)
1213 self
.dvifile
= dvifile
.DVIfile("%s.dvi" % self
.texfilename
, debug
=self
.dvidebug
)
1214 match
= self
.PyXBoxPattern
.search(self
.texmessage
)
1215 if not match
or int(match
.group("page")) != self
.page
:
1216 raise TexResultError("box extents not found", self
)
1217 left
, right
, height
, depth
= [float(xxx
)*72/72.27*unit
.x_pt
for xxx
in match
.group("lt", "rt", "ht", "dp")]
1218 box
= textbox(x
, y
, left
, right
, height
, depth
, self
.finishdvi
, fillstyles
)
1220 box
.reltransform(t
) # TODO: should trafos really use reltransform???
1221 # this is quite different from what we do elsewhere!!!
1222 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1224 box
.setdvicanvas(self
.dvifile
.readpage([ord("P"), ord("y"), ord("X"), self
.page
, 0, 0, 0, 0, 0, 0], fontmap
=fontmap
))
1226 box
.fontmap
= fontmap
1227 self
.needdvitextboxes
.append(box
)
1230 def text_pt(self
, x
, y
, expr
, *args
, **kwargs
):
1231 return self
.text(x
* unit
.t_pt
, y
* unit
.t_pt
, expr
, *args
, **kwargs
)
1233 PyXVariableBoxPattern
= re
.compile(r
"PyXVariableBox:page=(?P<page>\d+),par=(?P<par>\d+),prevgraf=(?P<prevgraf>\d+):")
1235 def textboxes(self
, text
, pageshapes
):
1236 # this is some experimental code to put text into several boxes
1237 # while the bounding shape changes from box to box (rectangles only)
1238 # first we load sev.tex
1239 if not self
.textboxesincluded
:
1240 self
.execute(r
"\input textboxes.tex", [texmessage
.load
])
1241 self
.textboxesincluded
= 1
1242 # define page shapes
1243 pageshapes_str
= "\\hsize=%.5ftruept%%\n\\vsize=%.5ftruept%%\n" % (72.27/72*unit
.topt(pageshapes
[0][0]), 72.27/72*unit
.topt(pageshapes
[0][1]))
1244 pageshapes_str
+= "\\lohsizes={%\n"
1245 for hsize
, vsize
in pageshapes
[1:]:
1246 pageshapes_str
+= "{\\global\\hsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(hsize
))
1247 pageshapes_str
+= "{\\relax}%\n}%\n"
1248 pageshapes_str
+= "\\lovsizes={%\n"
1249 for hsize
, vsize
in pageshapes
[1:]:
1250 pageshapes_str
+= "{\\global\\vsize=%.5ftruept}%%\n" % (72.27/72*unit
.topt(vsize
))
1251 pageshapes_str
+= "{\\relax}%\n}%\n"
1257 self
.execute(pageshapes_str
, [])
1258 parnos_str
= "}{".join(parnos
)
1260 parnos_str
= "{%s}" % parnos_str
1261 parnos_str
= "\\parnos={%s{\\relax}}%%\n" % parnos_str
1262 self
.execute(parnos_str
, [])
1263 parshapes_str
= "\\parshapes={%%\n%s%%\n{\\relax}%%\n}%%\n" % "%\n".join(parshapes
)
1264 self
.execute(parshapes_str
, [])
1265 self
.execute("\\global\\count0=1%%\n"
1266 "\\global\\parno=0%%\n"
1267 "\\global\\myprevgraf=0%%\n"
1268 "\\global\\showprevgraf=0%%\n"
1269 "\\global\\outputtype=0%%\n"
1270 "\\global\\leastcost=10000000%%\n"
1272 "\\vfill\\supereject%%\n" % text
, [texmessage
.ignore
])
1274 if self
.dvifile
is None:
1275 self
.dvifile
= dvifile
.DVIfile("%s.dvi" % self
.texfilename
, debug
=self
.dvidebug
)
1277 raise RuntimeError("textboxes currently needs texipc")
1280 lastparshapes
= parshapes
1283 lastpar
= prevgraf
= -1
1284 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
)
1287 page
= int(m
.group("page"))
1288 assert page
== pages
1289 par
= int(m
.group("par"))
1290 prevgraf
= int(m
.group("prevgraf"))
1291 if page
<= len(pageshapes
):
1292 width
= 72.27/72*unit
.topt(pageshapes
[page
-1][0])
1294 width
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1295 if page
< len(pageshapes
):
1296 nextwidth
= 72.27/72*unit
.topt(pageshapes
[page
][0])
1298 nextwidth
= 72.27/72*unit
.topt(pageshapes
[-1][0])
1301 # a new paragraph is to be broken
1302 parnos
.append(str(par
))
1303 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
)])
1305 parshape
= " 0pt " + parshape
1306 parshapes
.append("{\\parshape %i%s 0pt %.5ftruept}" % (prevgraf
+ 1, parshape
, nextwidth
))
1307 elif prevgraf
== lastprevgraf
:
1310 # we have to append the breaking of the previous paragraph
1311 oldparshape
= " ".join(parshapes
[-1].split(' ')[2:2+2*lastprevgraf
])
1312 oldparshape
= oldparshape
.split('}')[0]
1314 oldparshape
= " " + oldparshape
1315 parshape
= " 0pt ".join(["%.5ftruept" % width
for i
in range(prevgraf
- lastprevgraf
)])
1317 parshape
= " 0pt " + parshape
1320 parshapes
[-1] = "{\\parshape %i%s%s 0pt %.5ftruept}" % (prevgraf
+ 1, oldparshape
, parshape
, nextwidth
)
1322 lastprevgraf
= prevgraf
1324 m
= self
.PyXVariableBoxPattern
.search(self
.texmessage
, nextpos
)
1326 for i
in range(pages
):
1327 result
.append(self
.dvifile
.readpage([i
+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
1328 if parnos
== lastparnos
and parshapes
== lastparshapes
:
1332 raise TexResultError("Too many loops in textboxes ", texrunner
)
1335 # the module provides an default texrunner and methods for direct access
1336 defaulttexrunner
= texrunner()
1337 reset
= defaulttexrunner
.reset
1338 set = defaulttexrunner
.set
1339 preamble
= defaulttexrunner
.preamble
1340 text
= defaulttexrunner
.text
1341 text_pt
= defaulttexrunner
.text_pt
1343 def escapestring(s
, replace
={" ": "~",
1355 "\\": "{$\setminus$}",
1357 "escape all ascii characters such that they are printable by TeX/LaTeX"
1360 if not 32 <= ord(s
[i
]) < 127:
1361 raise ValueError("escapestring function handles ascii strings only")
1368 s
= s
[:i
] + r
+ s
[i
+1:]