implement a different abort criteria for midpointsplit when doing intersections
[PyX.git] / pyx / text.py
blobb5dc336a7841cccf8eea90425249e56feff96741
1 # Copyright (C) 2002-2013 Jörg Lehmann <joergl@users.sourceforge.net>
2 # Copyright (C) 2003-2011 Michael Schindler <m-schindler@users.sourceforge.net>
3 # Copyright (C) 2002-2013 André Wobst <wobsta@users.sourceforge.net>
5 # This file is part of PyX (http://pyx.sourceforge.net/).
7 # PyX is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # PyX is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with PyX; if not, write to the Free Software
19 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
21 import atexit, errno, functools, glob, inspect, io, itertools, logging, os
22 import queue, re, shutil, sys, tempfile, textwrap, threading
24 from pyx import config, unit, box, baseclasses, trafo, version, attr, style, path
25 from pyx import bbox as bboxmodule
26 from pyx.dvi import dvifile
28 logger = logging.getLogger("pyx")
30 def remove_string(p, s):
31 """Removes a string from a string.
33 The function removes the first occurrence of a string in another string.
35 :param str p: string to be removed
36 :param str s: string to be searched
37 :returns: tuple of the result string and a success boolean (``True`` when
38 the string was removed)
39 :rtype: tuple of str and bool
41 Example:
42 >>> remove_string("XXX", "abcXXXdefXXXghi")
43 ('abcdefXXXghi', True)
45 """
46 r = s.replace(p, '', 1)
47 return r, r != s
50 def remove_pattern(p, s, ignore_nl=True):
51 r"""Removes a pattern from a string.
53 The function removes the first occurence of the pattern from a string. It
54 returns a tuple of the resulting string and the matching object (or
55 ``None``, if the pattern did not match).
57 :param re.regex p: pattern to be removed
58 :param str s: string to be searched
59 :param bool ignore_nl: When ``True``, newlines in the string are ignored
60 during the pattern search. The returned string will still contain all
61 newline characters outside of the matching part of the string, whereas
62 the returned matching object will not contain the newline characters
63 inside of the matching part of the string.
64 :returns: the result string and the match object or ``None`` if
65 search failed
66 :rtype: tuple of str and (re.match or None)
68 Example:
69 >>> r, m = remove_pattern(re.compile("XXX"), 'ab\ncXX\nXdefXX\nX')
70 >>> r
71 'ab\ncdefXX\nX'
72 >>> m.string[m.start():m.end()]
73 'XXX'
75 """
76 if ignore_nl:
77 r = s.replace('\n', '')
78 has_nl = r != s
79 else:
80 r = s
81 has_nl = False
82 m = p.search(r)
83 if m:
84 s_start = r_start = m.start()
85 s_end = r_end = m.end()
86 if has_nl:
87 j = 0
88 for c in s:
89 if c == '\n':
90 if j < r_end:
91 s_end += 1
92 if j <= r_start:
93 s_start += 1
94 else:
95 j += 1
96 return s[:s_start] + s[s_end:], m
97 return s, None
100 def index_all(c, s):
101 """Return list of positions of a character in a string.
103 Example:
104 >>> index_all("X", "abXcdXef")
105 [2, 5]
108 assert len(c) == 1
109 return [i for i, x in enumerate(s) if x == c]
112 def pairwise(i):
113 """Returns iterator over pairs of data from an iterable.
115 Example:
116 >>> list(pairwise([1, 2, 3]))
117 [(1, 2), (2, 3)]
120 a, b = itertools.tee(i)
121 next(b, None)
122 return zip(a, b)
125 def remove_nested_brackets(s, openbracket="(", closebracket=")", quote='"'):
126 """Remove nested brackets
128 Return a modified string with all nested brackets 1 removed, i.e. only
129 keep the first bracket nesting level. In case an opening bracket is
130 immediately followed by a quote, the quoted string is left untouched,
131 even if it contains brackets. The use-case for that are files in the
132 folder "Program Files (x86)".
134 If the bracket nesting level is broken (unbalanced), the unmodified
135 string is returned.
137 Example:
138 >>> remove_nested_brackets('aaa("bb()bb" cc(dd(ee))ff)ggg'*2)
139 'aaa("bb()bb" ccff)gggaaa("bb()bb" ccff)ggg'
142 openpos = index_all(openbracket, s)
143 closepos = index_all(closebracket, s)
144 if quote is not None:
145 quotepos = index_all(quote, s)
146 for openquote, closequote in pairwise(quotepos):
147 if openquote-1 in openpos:
148 # ignore brackets in quoted string
149 openpos = [pos for pos in openpos
150 if not (openquote < pos < closequote)]
151 closepos = [pos for pos in closepos
152 if not (openquote < pos < closequote)]
153 if len(openpos) != len(closepos):
154 # unbalanced brackets
155 return s
157 # keep the original string in case we need to return due to broken nesting levels
158 r = s
160 level = 0
161 # Iterate over the bracket positions from the end.
162 # We go reversely to be able to immediately remove nested bracket levels
163 # without influencing bracket positions yet to come in the loop.
164 for pos, leveldelta in sorted(itertools.chain(zip(openpos, itertools.repeat(-1)),
165 zip(closepos, itertools.repeat(1))),
166 reverse=True):
167 # the current bracket nesting level
168 level += leveldelta
169 if level < 0:
170 # unbalanced brackets
171 return s
172 if leveldelta == 1 and level == 2:
173 # a closing bracket to cut after
174 endpos = pos+1
175 if leveldelta == -1 and level == 1:
176 # an opening bracket to cut at -> remove
177 r = r[:pos] + r[endpos:]
178 return r
181 class TexResultError(ValueError):
182 "Error raised by :class:`texmessage` parsers."
183 pass
186 class texmessage:
187 """Collection of TeX output parsers.
189 This class is not meant to be instanciated. Instead, it serves as a
190 namespace for TeX output parsers, which are functions receiving a TeX
191 output and returning parsed output.
193 In addition, this class also contains some generator functions (namely
194 :attr:`texmessage.no_file` and :attr:`texmessage.pattern`), which return a
195 function according to the given parameters. They are used to generate some
196 of the parsers in this class and can be used to create others as well.
199 start_pattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
201 @staticmethod
202 def start(msg):
203 r"""Validate TeX/LaTeX startup message including scrollmode test.
205 Example:
206 >>> texmessage.start(r'''
207 ... This is e-TeX (version)
208 ... *! Undefined control sequence.
209 ... <*> \raiseerror
210 ... %
211 ... ''', 0)
215 # check for "This is e-TeX" etc.
216 if not texmessage.start_pattern.search(msg):
217 raise TexResultError("TeX startup failed")
219 # check for \raiseerror -- just to be sure that communication works
220 new = msg.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
221 if msg == new:
222 raise TexResultError("TeX scrollmode check failed")
223 return new
225 @staticmethod
226 def no_file(fileending, qualname=None):
227 "Generator function to ignore the missing file message for fileending."
228 def check(msg):
229 "Ignore the missing {} file message."
230 return msg.replace("No file texput.%s." % fileending, "").replace("No file %s%stexput.%s." % (os.curdir, os.sep, fileending), "")
231 check.__doc__ = check.__doc__.format(fileending)
232 if qualname is not None:
233 check.__qualname__ = qualname
234 return check
236 no_aux = staticmethod(no_file.__func__("aux", "texmessage.no_aux"))
237 no_nav = staticmethod(no_file.__func__("nav", "texmessage.no_nav"))
239 aux_pattern = re.compile(r'\(([^()]+\.aux|"[^"]+\.aux")\)')
240 log_pattern = re.compile(r"Transcript written on .*texput\.log\.", re.DOTALL)
242 @staticmethod
243 def end(msg):
244 "Validate TeX shutdown message."
245 msg = re.sub(texmessage.aux_pattern, "", msg).replace("(see the transcript file for additional information)", "")
247 # check for "Transcript written on ...log."
248 msg, m = remove_pattern(texmessage.log_pattern, msg)
249 if not m:
250 raise TexResultError("TeX logfile message expected")
251 return msg
253 quoted_file_pattern = re.compile(r'\("(?P<filename>[^"]+)".*?\)')
254 file_pattern = re.compile(r'\((?P<filename>[^"][^ )]*).*?\)', re.DOTALL)
256 @staticmethod
257 def load(msg):
258 """Ignore file loading messages.
260 Removes text starting with a round bracket followed by a filename
261 ignoring all further text until the corresponding closing bracket.
262 Quotes and/or line breaks in the filename are handled as needed for TeX
263 output.
265 Without quoting the filename, the necessary removal of line breaks is
266 not well defined and the different possibilities are tested to check
267 whether one solution is ok. The last of the examples below checks this
268 behavior.
270 Examples:
271 >>> texmessage.load(r'''other (text.py) things''', 0)
272 'other things'
273 >>> texmessage.load(r'''other ("text.py") things''', 0)
274 'other things'
275 >>> texmessage.load(r'''other ("tex
276 ... t.py" further (ignored)
277 ... text) things''', 0)
278 'other things'
279 >>> texmessage.load(r'''other (t
280 ... ext
281 ... .py
282 ... fur
283 ... ther (ignored) text) things''', 0)
284 'other things'
287 r = remove_nested_brackets(msg)
288 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
289 while m:
290 if not os.path.isfile(m.group("filename")):
291 return msg
292 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
293 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
294 while m:
295 for filename in itertools.accumulate(m.group("filename").split("\n")):
296 if os.path.isfile(filename):
297 break
298 else:
299 return msg
300 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
301 return r
303 quoted_def_pattern = re.compile(r'\("(?P<filename>[^"]+\.(fd|def))"\)')
304 def_pattern = re.compile(r'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
306 @staticmethod
307 def load_def(msg):
308 "Ignore font definition (``*.fd`` and ``*.def``) loading messages."
309 r = msg
310 for p in [texmessage.quoted_def_pattern, texmessage.def_pattern]:
311 r, m = remove_pattern(p, r)
312 while m:
313 if not os.path.isfile(m.group("filename")):
314 return msg
315 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
316 return r
318 quoted_graphics_pattern = re.compile(r'<"(?P<filename>[^"]+\.eps)">')
319 graphics_pattern = re.compile(r'<(?P<filename>[^"][^>]*\.eps)>')
321 @staticmethod
322 def load_graphics(msg):
323 "Ignore graphics file (``*.eps``) loading messages."
324 r = msg
325 for p in [texmessage.quoted_graphics_pattern, texmessage.graphics_pattern]:
326 r, m = remove_pattern(p, r)
327 while m:
328 if not os.path.isfile(m.group("filename")):
329 return msg
330 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
331 return r
333 @staticmethod
334 def ignore(msg):
335 """Ignore all messages.
337 Should be used as a last resort only. You should write a proper TeX
338 output parser function for the output you observe.
341 return ""
343 @staticmethod
344 def warn(msg):
345 """Warn about all messages.
347 Similar to :attr:`ignore`, but writing a warning to the logger about
348 the TeX output. This is considered to be better when you need to get it
349 working quickly as you will still be prompted about the unresolved
350 output, while the processing continues.
353 if msg:
354 logger.warning("ignoring TeX warnings:\n%s" % textwrap.indent(msg.rstrip(), " "))
355 return ""
357 @staticmethod
358 def pattern(p, warning, qualname=None):
359 "Warn by regular expression pattern matching."
360 def check(msg):
361 "Warn about {}."
362 msg, m = remove_pattern(p, msg, ignore_nl=False)
363 while m:
364 logger.warning("ignoring %s:\n%s" % (warning, m.string[m.start(): m.end()].rstrip()))
365 msg, m = remove_pattern(p, msg, ignore_nl=False)
366 return msg
367 check.__doc__ = check.__doc__.format(warning)
368 if qualname is not None:
369 check.__qualname__ = qualname
370 return check
372 box_warning = staticmethod(pattern.__func__(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE),
373 "overfull/underfull box", qualname="texmessage.box_warning"))
374 font_warning = staticmethod(pattern.__func__(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE),
375 "font substitutions of NFSS", qualname="texmessage.font_warning"))
376 package_warning = staticmethod(pattern.__func__(re.compile(r"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re.MULTILINE | re.IGNORECASE),
377 "generic package messages", qualname="texmessage.package_warning"))
378 rerun_warning = staticmethod(pattern.__func__(re.compile(r"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re.MULTILINE),
379 "rerun required message", qualname="texmessage.rerun_warning"))
380 nobbl_warning = staticmethod(pattern.__func__(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE),
381 "no-bbl message", qualname="texmessage.nobbl_warning"))
384 ###############################################################################
385 # textattrs
386 ###############################################################################
388 _textattrspreamble = ""
390 class textattr:
391 "a textattr defines a apply method, which modifies a (La)TeX expression"
393 class _localattr: pass
395 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
396 \def\PyXragged{%
397 \leftskip=0pt plus \PyXFlushHAlign fil%
398 \rightskip=0pt plus 1fil%
399 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
400 \parfillskip=0pt%
401 \pretolerance=9999%
402 \tolerance=9999%
403 \parindent=0pt%
404 \hyphenpenalty=9999%
405 \exhyphenpenalty=9999}%
408 class boxhalign(attr.exclusiveattr, textattr, _localattr):
410 def __init__(self, aboxhalign):
411 self.boxhalign = aboxhalign
412 attr.exclusiveattr.__init__(self, boxhalign)
414 def apply(self, expr):
415 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
417 boxhalign.left = boxhalign(0)
418 boxhalign.center = boxhalign(0.5)
419 boxhalign.right = boxhalign(1)
420 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
423 class flushhalign(attr.exclusiveattr, textattr, _localattr):
425 def __init__(self, aflushhalign):
426 self.flushhalign = aflushhalign
427 attr.exclusiveattr.__init__(self, flushhalign)
429 def apply(self, expr):
430 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
432 flushhalign.left = flushhalign(0)
433 flushhalign.center = flushhalign(0.5)
434 flushhalign.right = flushhalign(1)
435 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
438 class halign(boxhalign, flushhalign, _localattr):
440 def __init__(self, aboxhalign, aflushhalign):
441 self.boxhalign = aboxhalign
442 self.flushhalign = aflushhalign
443 attr.exclusiveattr.__init__(self, halign)
445 def apply(self, expr):
446 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
448 halign.left = halign(0, 0)
449 halign.center = halign(0.5, 0.5)
450 halign.right = halign(1, 1)
451 halign.clear = attr.clearclass(halign)
452 halign.boxleft = boxhalign.left
453 halign.boxcenter = boxhalign.center
454 halign.boxright = boxhalign.right
455 halign.flushleft = halign.raggedright = flushhalign.left
456 halign.flushcenter = halign.raggedcenter = flushhalign.center
457 halign.flushright = halign.raggedleft = flushhalign.right
460 class _mathmode(attr.attr, textattr, _localattr):
461 "math mode"
463 def apply(self, expr):
464 return r"$\displaystyle{%s}$" % expr
466 mathmode = _mathmode()
467 clearmathmode = attr.clearclass(_mathmode)
470 class _phantom(attr.attr, textattr, _localattr):
471 "phantom text"
473 def apply(self, expr):
474 return r"\phantom{%s}" % expr
476 phantom = _phantom()
477 clearphantom = attr.clearclass(_phantom)
480 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
482 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
484 top = 1
485 middle = 2
486 bottom = 3
488 def __init__(self, width, baseline=top):
489 self.width = width * 72.27 / (unit.scale["x"] * 72)
490 self.baseline = baseline
491 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
493 def apply(self, expr):
494 if self.baseline == self.top:
495 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
496 elif self.baseline == self.middle:
497 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)
498 elif self.baseline == self.bottom:
499 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
500 else:
501 ValueError("invalid baseline argument")
503 parbox_pt.clear = attr.clearclass(parbox_pt)
505 class parbox(parbox_pt):
507 def __init__(self, width, **kwargs):
508 parbox_pt.__init__(self, unit.topt(width), **kwargs)
510 parbox.clear = parbox_pt.clear
513 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
515 class valign(attr.sortbeforeexclusiveattr, textattr):
517 def __init__(self, avalign):
518 self.valign = avalign
519 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
521 def apply(self, expr):
522 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)
524 valign.top = valign(0)
525 valign.middle = valign(0.5)
526 valign.bottom = valign(1)
527 valign.clear = valign.baseline = attr.clearclass(valign)
530 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
532 class _vshift(attr.sortbeforeattr, textattr):
534 def __init__(self):
535 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
537 def apply(self, expr):
538 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
540 class vshift(_vshift):
541 "vertical down shift by a fraction of a character height"
543 def __init__(self, lowerratio, heightstr="0"):
544 _vshift.__init__(self)
545 self.lowerratio = lowerratio
546 self.heightstr = heightstr
548 def setheightexpr(self):
549 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
551 class _vshiftmathaxis(_vshift):
552 "vertical down shift by the height of the math axis"
554 def setheightexpr(self):
555 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
558 vshift.bottomzero = vshift(0)
559 vshift.middlezero = vshift(0.5)
560 vshift.topzero = vshift(1)
561 vshift.mathaxis = _vshiftmathaxis()
562 vshift.clear = attr.clearclass(_vshift)
565 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
566 None, "tiny", "scriptsize", "footnotesize", "small"]
568 class size(attr.sortbeforeattr, textattr):
569 "font size"
571 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
572 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
573 raise ValueError("either specify sizeindex or sizename")
574 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
575 if sizeindex is not None:
576 if sizeindex >= 0 and sizeindex < sizelist.index(None):
577 self.size = sizelist[sizeindex]
578 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
579 self.size = sizelist[sizeindex]
580 else:
581 raise IndexError("index out of sizelist range")
582 else:
583 self.size = sizename
585 def apply(self, expr):
586 return r"\%s{}%s" % (self.size, expr)
588 size.tiny = size(-4)
589 size.scriptsize = size.script = size(-3)
590 size.footnotesize = size.footnote = size(-2)
591 size.small = size(-1)
592 size.normalsize = size.normal = size(0)
593 size.large = size(1)
594 size.Large = size(2)
595 size.LARGE = size(3)
596 size.huge = size(4)
597 size.Huge = size(5)
598 size.clear = attr.clearclass(size)
601 ###############################################################################
602 # texrunner
603 ###############################################################################
606 class MonitorOutput(threading.Thread):
608 def __init__(self, name, output):
609 """Deadlock-safe output stream reader and monitor.
611 This method sets up a thread to continously read lines from a stream.
612 By that a deadlock due to a full pipe is prevented. In addition, the
613 stream content can be monitored for containing a certain string (see
614 :meth:`expect` and :meth:`wait`) and return all the collected output
615 (see :meth:`read`).
617 :param string name: name to be used while logging in :meth:`wait` and
618 :meth:`done`
619 :param file output: output stream
622 self.output = output
623 self._expect = queue.Queue(1)
624 self._received = threading.Event()
625 self._output = queue.Queue()
626 threading.Thread.__init__(self, name=name, daemon=1)
627 self.start()
629 def expect(self, s):
630 """Expect a string on a **single** line in the output.
632 This method must be called **before** the output occurs, i.e. before
633 the input is written to the TeX/LaTeX process.
635 :param s: expected string or ``None`` if output is expected to become
636 empty
637 :type s: str or None
640 self._expect.put_nowait(s)
642 def read(self):
643 """Read all output collected since its previous call.
645 The output reading should be synchronized by the :meth:`expect`
646 and :meth:`wait` methods.
648 :returns: collected output from the stream
649 :rtype: str
652 l = []
653 try:
654 while True:
655 l.append(self._output.get_nowait())
656 except queue.Empty:
657 pass
658 return "".join(l).replace("\r\n", "\n").replace("\r", "\n")
660 def _wait(self, waiter, checker):
661 """Helper method to implement :meth:`wait` and :meth:`done`.
663 Waits for an event using the *waiter* and *checker* functions while
664 providing user feedback to the ``pyx``-logger using the warning level
665 according to the ``wait`` and ``showwait`` from the ``text`` section of
666 the pyx :mod:`config`.
668 :param function waiter: callback to wait for (the function gets called
669 with a timeout parameter)
670 :param function checker: callback returing ``True`` if
671 waiting was successful
672 :returns: ``True`` when wait was successful
673 :rtype: bool
676 wait = config.getint("text", "wait", 60)
677 showwait = config.getint("text", "showwait", 5)
678 if showwait:
679 waited = 0
680 hasevent = False
681 while waited < wait and not hasevent:
682 if wait - waited > showwait:
683 waiter(showwait)
684 waited += showwait
685 else:
686 waiter(wait - waited)
687 waited += wait - waited
688 hasevent = checker()
689 if not hasevent:
690 if waited < wait:
691 logger.warning("Still waiting for {} "
692 "after {} (of {}) seconds..."
693 .format(self.name, waited, wait))
694 else:
695 logger.warning("The timeout of {} seconds expired "
696 "and {} did not respond."
697 .format(waited, self.name))
698 return hasevent
699 else:
700 waiter(wait)
701 return checker()
703 def wait(self):
704 """Wait for the expected output to happen.
706 Waits either until a line containing the string set by the previous
707 :meth:`expect` call is found, or a timeout occurs.
709 :returns: ``True`` when the expected string was found
710 :rtype: bool
713 r = self._wait(self._received.wait, self._received.isSet)
714 if r:
715 self._received.clear()
716 return r
718 def done(self):
719 """Waits until the output becomes empty.
721 Waits either until the output becomes empty, or a timeout occurs.
722 The generated output can still be catched by :meth:`read` after
723 :meth:`done` was successful.
725 In the proper workflow :meth:`expect` should be called with ``None``
726 before the output completes, as otherwise a ``ValueError`` is raised
727 in the :meth:`run`.
729 :returns: ``True`` when the output has become empty
730 :rtype: bool
733 return self._wait(self.join, lambda self=self: not self.is_alive())
735 def _readline(self):
736 """Read a line from the output.
738 To be used **inside** the thread routine only.
740 :returns: one line of the output as a string
741 :rtype: str
744 while True:
745 try:
746 return self.output.readline()
747 except IOError as e:
748 if e.errno != errno.EINTR:
749 raise
751 def run(self):
752 """Thread routine.
754 **Not** to be called from outside.
756 :raises ValueError: output becomes empty while some string is expected
759 expect = None
760 while True:
761 line = self._readline()
762 if expect is None:
763 try:
764 expect = self._expect.get_nowait()
765 except queue.Empty:
766 pass
767 if not line:
768 break
769 self._output.put(line)
770 if expect is not None:
771 found = line.find(expect)
772 if found != -1:
773 self._received.set()
774 expect = None
775 self.output.close()
776 if expect is not None:
777 raise ValueError("{} finished unexpectedly".format(self.name))
780 class textbox(box.rect, baseclasses.canvasitem):
781 """basically a box.rect, but it contains a text created by the texrunner
782 - texrunner._text and texrunner.text return such an object
783 - _textbox instances can be inserted into a canvas
784 - the output is contained in a page of the dvifile available thru the texrunner"""
785 # TODO: shouldn't all boxes become canvases? how about inserts then?
787 def __init__(self, x, y, left, right, height, depth, do_finish, fontmap, singlecharmode, attrs):
789 - do_finish is a method to be called to get the dvicanvas
790 (e.g. the do_finish calls the setdvicanvas method)
791 - attrs are fillstyles"""
792 self.left = left
793 self.right = right
794 self.width = left + right
795 self.height = height
796 self.depth = depth
797 self.do_finish = do_finish
798 self.fontmap = fontmap
799 self.singlecharmode = singlecharmode
800 self.attrs = attrs
802 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
803 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
805 self.dvicanvas = None
806 self.insertdvicanvas = False
808 def transform(self, *trafos):
809 if self.dvicanvas is not None:
810 raise ValueError("can't apply after dvicanvas was inserted")
811 box.rect.transform(self, *trafos)
812 for trafo in trafos:
813 self.texttrafo = trafo * self.texttrafo
815 def readdvipage(self, dvifile, page):
816 self.dvicanvas = dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0],
817 fontmap=self.fontmap, singlecharmode=self.singlecharmode, attrs=[self.texttrafo] + self.attrs)
819 def marker(self, marker):
820 self.do_finish()
821 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
823 def textpath(self):
824 self.do_finish()
825 textpath = path.path()
826 for item in self.dvicanvas.items:
827 textpath += item.textpath()
828 return textpath.transformed(self.texttrafo)
830 def processPS(self, file, writer, context, registry, bbox):
831 self.do_finish()
832 abbox = bboxmodule.empty()
833 self.dvicanvas.processPS(file, writer, context, registry, abbox)
834 bbox += box.rect.bbox(self)
836 def processPDF(self, file, writer, context, registry, bbox):
837 self.do_finish()
838 abbox = bboxmodule.empty()
839 self.dvicanvas.processPDF(file, writer, context, registry, abbox)
840 bbox += box.rect.bbox(self)
843 class _marker:
844 pass
847 class errordetail:
848 "Constants defining the verbosity of the :exc:`TexResultError`."
849 none = 0 #: Without any input and output.
850 default = 1 #: Input and parsed output shortend to 5 lines.
851 full = 2 #: Full input and unparsed as well as parsed output.
854 class Tee(object):
856 def __init__(self, *files):
857 self.files = files
859 def write(self, data):
860 for file in self.files:
861 file.write(data)
863 def flush(self):
864 for file in self.files:
865 file.flush()
867 def close(self):
868 for file in self.files:
869 file.close()
871 # The texrunner state represents the next (or current) execute state.
872 STATE_START, STATE_PREAMBLE, STATE_TYPESET, STATE_DONE = range(4)
873 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:")
874 dvi_pattern = re.compile(r"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re.DOTALL)
876 class TexDoneError(Exception):
877 pass
880 class SingleRunner:
882 texmessages_start_default = [texmessage.start]
883 #: default :class:`texmessage` parsers for interpreter startup
884 texmessages_start_default = [texmessage.start]
885 #: default :class:`texmessage` parsers for interpreter shutdown
886 texmessages_end_default = [texmessage.end, texmessage.font_warning, texmessage.rerun_warning, texmessage.nobbl_warning]
887 #: default :class:`texmessage` parsers for preamble output
888 texmessages_preamble_default = [texmessage.load]
889 #: default :class:`texmessage` parsers for typeset output
890 texmessages_run_default = [texmessage.font_warning, texmessage.box_warning, texmessage.package_warning,
891 texmessage.load_def, texmessage.load_graphics]
893 def __init__(self, executable,
894 texenc="ascii",
895 usefiles=[],
896 texipc=config.getboolean("text", "texipc", 0),
897 copyinput=None,
898 dvitype=False,
899 errordetail=errordetail.default,
900 texmessages_start=[],
901 texmessages_end=[],
902 texmessages_preamble=[],
903 texmessages_run=[]):
904 """Base class for the TeX interface.
906 .. note:: This class cannot be used directly. It is the base class for
907 all texrunners and provides most of the implementation.
908 Still, to the end user the parameters except for *executable*
909 are important, as they are preserved in derived classes
910 usually.
912 :param str executable: command to start the TeX interpreter
913 :param str texenc: encoding to use in the communication with the TeX
914 interpreter
915 :param usefiles: list of supplementary files
916 :type usefiles: list of str
917 :param bool texipc: :ref:`texipc` flag.
918 :param copyinput: filename or file to be used to store a copy of all
919 the input passed to the TeX interpreter
920 :type copyinput: None or str or file
921 :param bool dvitype: flag to turn on dvitype-like output
922 :param errordetail: verbosity of the :exc:`TexResultError`
923 :type errordetail: :class:`errordetail`
924 :param texmessages_start: additional message parsers at interpreter
925 startup
926 :type texmessages_start: list of :class:`texmessage` parsers
927 :param texmessages_end: additional message parsers at interpreter
928 shutdown
929 :type texmessages_end: list of :class:`texmessage` parsers
930 :param texmessages_preamble: additional message parsers for preamble
931 output
932 :type texmessages_preamble: list of :class:`texmessage` parsers
933 :param texmessages_run: additional message parsers for typset output
934 :type texmessages_run: list of :class:`texmessage` parsers
936 self.executable = executable
937 self.texenc = texenc
938 self.usefiles = usefiles
939 self.texipc = texipc
940 self.copyinput = copyinput
941 self.dvitype = dvitype
942 self.errordetail = errordetail
943 self.texmessages_start = texmessages_start
944 self.texmessages_end = texmessages_end
945 self.texmessages_preamble = texmessages_preamble
946 self.texmessages_run = texmessages_run
948 self.state = STATE_START
949 self.executeid = 0
950 self.page = 0
952 self.needdvitextboxes = [] # when texipc-mode off
953 self.dvifile = None
955 self.tmpdir = None
957 def _cleanup(self):
958 """Clean-up TeX interpreter and tmp directory.
960 This funtion is hooked up in atexit to quit the TeX interpreter, to
961 save contents of usefiles, and to remove the temporary directory.
964 try:
965 if self.state > STATE_START:
966 if self.state < STATE_DONE:
967 self.do_finish()
968 if self.state < STATE_DONE: # cleanup while TeX is still running?
969 self.texoutput.expect(None)
970 self.force_done()
971 for f, msg in [(self.texinput.close, "We tried to communicate to %s to quit itself, but this seem to have failed. Trying by terminate signal now ...".format(self.name)),
972 (self.popen.terminate, "Failed, too. Trying by kill signal now ..."),
973 (self.popen.kill, "We tried very hard to get rid of the %s process, but we ultimately failed (as far as we can tell). Sorry.".format(self.name))]:
975 if self.texoutput.done():
976 break
977 logger.warning(msg)
978 for usefile in self.usefiles:
979 extpos = usefile.rfind(".")
980 try:
981 os.rename(os.path.join(self.tmpdir, "texput" + usefile[extpos:]), usefile)
982 except EnvironmentError:
983 logger.warning("Could not save '{}'.".format(usefile))
984 if os.path.isfile(usefile):
985 try:
986 os.unlink(usefile)
987 except EnvironmentError:
988 logger.warning("Failed to remove spurious file '{}'.".format(usefile))
989 finally:
990 shutil.rmtree(self.tmpdir, ignore_errors=True)
992 def _execute(self, expr, texmessages, oldstate, newstate):
993 """executes expr within TeX/LaTeX"""
994 assert STATE_PREAMBLE <= oldstate <= STATE_TYPESET
995 assert oldstate == self.state
996 assert newstate >= oldstate
997 if newstate == STATE_DONE:
998 self.texoutput.expect(None)
999 self.texinput.write(expr)
1000 else:
1001 if oldstate == newstate == STATE_TYPESET:
1002 self.page += 1
1003 expr = "\\ProcessPyXBox{%s%%\n}{%i}" % (expr, self.page)
1004 self.executeid += 1
1005 self.texoutput.expect("PyXInputMarker:executeid=%i:" % self.executeid)
1006 expr += "%%\n\\PyXInput{%i}%%\n" % self.executeid
1007 self.texinput.write(expr)
1008 self.texinput.flush()
1009 self.state = newstate
1010 if newstate == STATE_DONE:
1011 wait_ok = self.texoutput.done()
1012 else:
1013 wait_ok = self.texoutput.wait()
1014 try:
1015 parsed = unparsed = self.texoutput.read()
1016 if not wait_ok:
1017 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1018 if newstate != STATE_DONE:
1019 parsed, m = remove_string("PyXInputMarker:executeid=%s:" % self.executeid, parsed)
1020 if not m:
1021 raise TexResultError("PyXInputMarker expected")
1022 if oldstate == newstate == STATE_TYPESET:
1023 parsed, m = remove_pattern(PyXBoxPattern, parsed, ignore_nl=False)
1024 if not m:
1025 raise TexResultError("PyXBox expected")
1026 if m.group("page") != str(self.page):
1027 raise TexResultError("Wrong page number in PyXBox")
1028 extent = [float(x)*72/72.27*unit.x_pt for x in m.group("lt", "rt", "ht", "dp")]
1029 parsed, m = remove_string("[80.121.88.%s]" % self.page, parsed)
1030 if not m:
1031 raise TexResultError("PyXPageOutMarker expected")
1032 else:
1033 # check for "Output written on ...dvi (1 page, 220 bytes)."
1034 if self.page:
1035 parsed, m = remove_pattern(dvi_pattern, parsed)
1036 if not m:
1037 raise TexResultError("TeX dvifile messages expected")
1038 if m.group("page") != str(self.page):
1039 raise TexResultError("wrong number of pages reported")
1040 else:
1041 parsed, m = remove_string("No pages of output.", parsed)
1042 if not m:
1043 raise TexResultError("no dvifile expected")
1045 for t in texmessages:
1046 parsed = t(parsed)
1047 if parsed.replace(r"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1048 raise TexResultError("unhandled TeX response (might be an error)")
1049 except TexResultError as e:
1050 if self.errordetail > errordetail.none:
1051 def add(msg): e.args = (e.args[0] + msg,)
1052 add("\nThe expression passed to TeX was:\n{}".format(textwrap.indent(expr.rstrip(), " ")))
1053 if self.errordetail == errordetail.full:
1054 add("\nThe return message from TeX was:\n{}".format(textwrap.indent(unparsed.rstrip(), " ")))
1055 if self.errordetail == errordetail.default:
1056 if parsed.count('\n') > 6:
1057 parsed = "\n".join(parsed.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1058 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(textwrap.indent(parsed.rstrip(), " ")))
1059 raise e
1060 if oldstate == newstate == STATE_TYPESET:
1061 return extent
1063 def do_start(self):
1064 assert self.state == STATE_START
1065 self.state = STATE_PREAMBLE
1067 if self.tmpdir is None:
1068 self.tmpdir = tempfile.mkdtemp()
1069 atexit.register(self._cleanup)
1070 for usefile in self.usefiles:
1071 extpos = usefile.rfind(".")
1072 try:
1073 os.rename(usefile, os.path.join(self.tmpdir, "texput" + usefile[extpos:]))
1074 except OSError:
1075 pass
1076 cmd = [self.executable, '--output-directory', self.tmpdir]
1077 if self.texipc:
1078 cmd.append("--ipc")
1079 self.popen = config.Popen(cmd, stdin=config.PIPE, stdout=config.PIPE, stderr=config.STDOUT, bufsize=0)
1080 self.texinput = io.TextIOWrapper(self.popen.stdin, encoding=self.texenc)
1081 if self.copyinput:
1082 try:
1083 self.copyinput.write
1084 except AttributeError:
1085 self.texinput = Tee(open(self.copyinput, "w", encoding=self.texenc), self.texinput)
1086 else:
1087 self.texinput = Tee(self.copyinput, self.texinput)
1088 self.texoutput = MonitorOutput(self.name, io.TextIOWrapper(self.popen.stdout, encoding=self.texenc))
1089 self._execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1090 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1091 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1092 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1093 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1094 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1095 "\\newdimen\\PyXDimenHAlignRT%\n" +
1096 _textattrspreamble + # insert preambles for textattrs macros
1097 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1098 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1099 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1100 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1101 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1102 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1103 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1104 "lt=\\the\\PyXDimenHAlignLT,"
1105 "rt=\\the\\PyXDimenHAlignRT,"
1106 "ht=\\the\\ht\\PyXBox,"
1107 "dp=\\the\\dp\\PyXBox:}%\n"
1108 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1109 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1110 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1111 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1112 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1113 self.texmessages_start_default + self.texmessages_start, STATE_PREAMBLE, STATE_PREAMBLE)
1115 def do_preamble(self, expr, texmessages):
1116 if self.state < STATE_PREAMBLE:
1117 self.do_start()
1118 self._execute(expr, texmessages, STATE_PREAMBLE, STATE_PREAMBLE)
1120 def do_typeset(self, expr, texmessages):
1121 if self.state < STATE_PREAMBLE:
1122 self.do_start()
1123 if self.state < STATE_TYPESET:
1124 self.go_typeset()
1125 return self._execute(expr, texmessages, STATE_TYPESET, STATE_TYPESET)
1127 def do_finish(self):
1128 if self.state == STATE_DONE:
1129 return
1130 if self.state < STATE_TYPESET:
1131 self.go_typeset()
1132 self.go_finish()
1133 self.texinput.close() # close the input queue and
1134 self.texoutput.done() # wait for finish of the output
1136 if not self.texipc:
1137 dvifilename = os.path.join(self.tmpdir, "texput.dvi")
1138 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvitype)
1139 page = 1
1140 for box in self.needdvitextboxes:
1141 box.readdvipage(self.dvifile, page)
1142 page += 1
1143 if self.dvifile.readpage(None) is not None:
1144 raise ValueError("end of dvifile expected but further pages follow")
1146 def preamble(self, expr, texmessages=[]):
1147 r"""Execute a preamble.
1149 :param str expr: expression to be executed
1150 :param texmessages: additional message parsers
1151 :type texmessages: list of :class:`texmessage` parsers
1153 Preambles must not generate output, but are used to load files, perform
1154 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1155 before ``\begin{document}``. The method can be called multiple times,
1156 but only prior to :meth:`SingleRunner.text` and
1157 :meth:`SingleRunner.text_pt`.
1160 texmessages = self.texmessages_preamble_default + self.texmessages_preamble + texmessages
1161 self.do_preamble(expr, texmessages)
1163 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1164 """create text by passing expr to TeX/LaTeX
1165 - returns a textbox containing the result from running expr thru TeX/LaTeX
1166 - the box center is set to x, y
1167 - *args may contain attr parameters, namely:
1168 - textattr instances
1169 - texmessage instances
1170 - trafo._trafo instances
1171 - style.fillstyle instances"""
1172 if self.state == STATE_DONE:
1173 raise TexDoneError("typesetting process was terminated already")
1174 textattrs = attr.mergeattrs(textattrs) # perform cleans
1175 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1176 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1177 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1178 textattrs = attr.getattrs(textattrs, [textattr])
1179 for ta in textattrs[::-1]:
1180 expr = ta.apply(expr)
1181 first = self.state < STATE_TYPESET
1182 left, right, height, depth = self.do_typeset(expr, self.texmessages_run_default + self.texmessages_run + texmessages)
1183 if self.texipc and first:
1184 self.dvifile = dvifile.DVIfile(os.path.join(self.tmpdir, "texput.dvi"), debug=self.dvitype)
1185 box = textbox(x, y, left, right, height, depth, self.do_finish, fontmap, singlecharmode, fillstyles)
1186 for t in trafos:
1187 box.reltransform(t) # TODO: should trafos really use reltransform???
1188 # this is quite different from what we do elsewhere!!!
1189 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1190 if self.texipc:
1191 box.readdvipage(self.dvifile, self.page)
1192 else:
1193 self.needdvitextboxes.append(box)
1194 return box
1196 def text_pt(self, x, y, expr, *args, **kwargs):
1197 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1200 class SingleTexRunner(SingleRunner):
1202 def __init__(self, executable=config.get("text", "tex", "tex"), lfs="10pt", **kwargs):
1203 super().__init__(executable=executable, **kwargs)
1204 self.lfs = lfs
1205 self.name = "TeX"
1207 def go_typeset(self):
1208 assert self.state == STATE_PREAMBLE
1209 self.state = STATE_TYPESET
1211 def go_finish(self):
1212 self._execute("\\end%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1214 def force_done(self):
1215 self.texinput.write("\n\\end\n")
1217 def do_start(self):
1218 super().do_start()
1219 if self.lfs:
1220 if not self.lfs.endswith(".lfs"):
1221 self.lfs = "%s.lfs" % self.lfs
1222 with config.open(self.lfs, []) as lfsfile:
1223 lfsdef = lfsfile.read().decode("ascii")
1224 self._execute(lfsdef, [], STATE_PREAMBLE, STATE_PREAMBLE)
1225 self._execute("\\normalsize%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1226 self._execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1229 class SingleLatexRunner(SingleRunner):
1231 texmessages_docclass_default = [texmessage.load]
1232 texmessages_begindoc_default = [texmessage.load, texmessage.no_aux]
1234 def __init__(self, executable=config.get("text", "latex", "latex"),
1235 docclass="article", docopt=None, pyxgraphics=True,
1236 texmessages_docclass=[], texmessages_begindoc=[], **kwargs):
1237 super().__init__(executable=executable, **kwargs)
1238 self.docclass = docclass
1239 self.docopt = docopt
1240 self.pyxgraphics = pyxgraphics
1241 self.texmessages_docclass = texmessages_docclass
1242 self.texmessages_begindoc = texmessages_begindoc
1243 self.name = "LaTeX"
1245 def go_typeset(self):
1246 self._execute("\\begin{document}", self.texmessages_begindoc_default + self.texmessages_begindoc, STATE_PREAMBLE, STATE_TYPESET)
1248 def go_finish(self):
1249 self._execute("\\end{document}%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1251 def force_done(self):
1252 self.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1254 def do_start(self):
1255 super().do_start()
1256 if self.pyxgraphics:
1257 with config.open("pyx.def", []) as source, open(os.path.join(self.tmpdir, "pyx.def"), "wb") as dest:
1258 dest.write(source.read())
1259 self._execute("\\makeatletter%\n"
1260 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1261 "\\def\\ProcessOptions{%\n"
1262 "\\def\\Gin@driver{" + self.tmpdir.replace(os.sep, "/") + "/pyx.def}%\n"
1263 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1264 "\\saveProcessOptions}%\n"
1265 "\\makeatother",
1266 [], STATE_PREAMBLE, STATE_PREAMBLE)
1267 if self.docopt is not None:
1268 self._execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
1269 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1270 else:
1271 self._execute("\\documentclass{%s}" % self.docclass,
1272 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1275 def reset_for_tex_done(f):
1276 @functools.wraps(f)
1277 def wrapped(self, *args, **kwargs):
1278 try:
1279 return f(self, *args, **kwargs)
1280 except TexDoneError:
1281 self.reset(reinit=True)
1282 return f(self, *args, **kwargs)
1283 return wrapped
1286 class MultiRunner:
1288 def __init__(self, cls, *args, **kwargs):
1289 self.cls = cls
1290 self.args = args
1291 self.kwargs = kwargs
1292 self.reset()
1294 def preamble(self, expr, texmessages=[]):
1295 self.preambles.append((expr, texmessages))
1296 self.instance.preamble(expr, texmessages)
1298 @reset_for_tex_done
1299 def text_pt(self, *args, **kwargs):
1300 return self.instance.text_pt(*args, **kwargs)
1302 @reset_for_tex_done
1303 def text(self, *args, **kwargs):
1304 return self.instance.text(*args, **kwargs)
1306 def reset(self, reinit=False):
1307 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1308 self.instance = self.cls(*self.args, **self.kwargs)
1309 if reinit:
1310 for expr, texmessages in self.preambles:
1311 self.instance.preamble(expr, texmessages)
1312 else:
1313 self.preambles = []
1316 class TexRunner(MultiRunner):
1318 def __init__(self, **kwargs):
1319 super().__init__(SingleTexRunner, **kwargs)
1322 class LatexRunner(MultiRunner):
1324 def __init__(self, **kwargs):
1325 super().__init__(SingleLatexRunner, **kwargs)
1328 # old, deprecated names:
1329 texrunner = TexRunner
1330 latexrunner = LatexRunner
1332 def set(mode="tex", **kwargs):
1333 # note: defaulttexrunner is deprecated
1334 global default_runner, defaulttexrunner, reset, preamble, text, text_pt
1335 mode = mode.lower()
1336 if mode == "tex":
1337 default_runner = defaulttexrunner = TexRunner(**kwargs)
1338 elif mode == "latex":
1339 default_runner = defaulttexrunner = LatexRunner(**kwargs)
1340 else:
1341 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1342 reset = default_runner.reset
1343 preamble = default_runner.preamble
1344 text = default_runner.text
1345 text_pt = default_runner.text_pt
1347 set()
1349 def escapestring(s, replace={" ": "~",
1350 "$": "\\$",
1351 "&": "\\&",
1352 "#": "\\#",
1353 "_": "\\_",
1354 "%": "\\%",
1355 "^": "\\string^",
1356 "~": "\\string~",
1357 "<": "{$<$}",
1358 ">": "{$>$}",
1359 "{": "{$\{$}",
1360 "}": "{$\}$}",
1361 "\\": "{$\setminus$}",
1362 "|": "{$\mid$}"}):
1363 "escape all ascii characters such that they are printable by TeX/LaTeX"
1364 i = 0
1365 while i < len(s):
1366 if not 32 <= ord(s[i]) < 127:
1367 raise ValueError("escapestring function handles ascii strings only")
1368 c = s[i]
1369 try:
1370 r = replace[c]
1371 except KeyError:
1372 i += 1
1373 else:
1374 s = s[:i] + r + s[i+1:]
1375 i += len(r)
1376 return s