fix for missing textwrap.indent on Python 3.2 reported by Michael Schindler
[PyX.git] / pyx / text.py
blob03fa2f649f327c666f3e0a41c26e02415ff346c5
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")
31 def indent_text(text):
32 "Prepends two spaces to each line in text."
33 return "".join(" " + line for line in text.splitlines(True))
36 def remove_string(p, s):
37 """Removes a string from a string.
39 The function removes the first occurrence of a string in another string.
41 :param str p: string to be removed
42 :param str s: string to be searched
43 :returns: tuple of the result string and a success boolean (``True`` when
44 the string was removed)
45 :rtype: tuple of str and bool
47 Example:
48 >>> remove_string("XXX", "abcXXXdefXXXghi")
49 ('abcdefXXXghi', True)
51 """
52 r = s.replace(p, '', 1)
53 return r, r != s
56 def remove_pattern(p, s, ignore_nl=True):
57 r"""Removes a pattern from a string.
59 The function removes the first occurence of the pattern from a string. It
60 returns a tuple of the resulting string and the matching object (or
61 ``None``, if the pattern did not match).
63 :param re.regex p: pattern to be removed
64 :param str s: string to be searched
65 :param bool ignore_nl: When ``True``, newlines in the string are ignored
66 during the pattern search. The returned string will still contain all
67 newline characters outside of the matching part of the string, whereas
68 the returned matching object will not contain the newline characters
69 inside of the matching part of the string.
70 :returns: the result string and the match object or ``None`` if
71 search failed
72 :rtype: tuple of str and (re.match or None)
74 Example:
75 >>> r, m = remove_pattern(re.compile("XXX"), 'ab\ncXX\nXdefXX\nX')
76 >>> r
77 'ab\ncdefXX\nX'
78 >>> m.string[m.start():m.end()]
79 'XXX'
81 """
82 if ignore_nl:
83 r = s.replace('\n', '')
84 has_nl = r != s
85 else:
86 r = s
87 has_nl = False
88 m = p.search(r)
89 if m:
90 s_start = r_start = m.start()
91 s_end = r_end = m.end()
92 if has_nl:
93 j = 0
94 for c in s:
95 if c == '\n':
96 if j < r_end:
97 s_end += 1
98 if j <= r_start:
99 s_start += 1
100 else:
101 j += 1
102 return s[:s_start] + s[s_end:], m
103 return s, None
106 def index_all(c, s):
107 """Return list of positions of a character in a string.
109 Example:
110 >>> index_all("X", "abXcdXef")
111 [2, 5]
114 assert len(c) == 1
115 return [i for i, x in enumerate(s) if x == c]
118 def pairwise(i):
119 """Returns iterator over pairs of data from an iterable.
121 Example:
122 >>> list(pairwise([1, 2, 3]))
123 [(1, 2), (2, 3)]
126 a, b = itertools.tee(i)
127 next(b, None)
128 return zip(a, b)
131 def remove_nested_brackets(s, openbracket="(", closebracket=")", quote='"'):
132 """Remove nested brackets
134 Return a modified string with all nested brackets 1 removed, i.e. only
135 keep the first bracket nesting level. In case an opening bracket is
136 immediately followed by a quote, the quoted string is left untouched,
137 even if it contains brackets. The use-case for that are files in the
138 folder "Program Files (x86)".
140 If the bracket nesting level is broken (unbalanced), the unmodified
141 string is returned.
143 Example:
144 >>> remove_nested_brackets('aaa("bb()bb" cc(dd(ee))ff)ggg'*2)
145 'aaa("bb()bb" ccff)gggaaa("bb()bb" ccff)ggg'
148 openpos = index_all(openbracket, s)
149 closepos = index_all(closebracket, s)
150 if quote is not None:
151 quotepos = index_all(quote, s)
152 for openquote, closequote in pairwise(quotepos):
153 if openquote-1 in openpos:
154 # ignore brackets in quoted string
155 openpos = [pos for pos in openpos
156 if not (openquote < pos < closequote)]
157 closepos = [pos for pos in closepos
158 if not (openquote < pos < closequote)]
159 if len(openpos) != len(closepos):
160 # unbalanced brackets
161 return s
163 # keep the original string in case we need to return due to broken nesting levels
164 r = s
166 level = 0
167 # Iterate over the bracket positions from the end.
168 # We go reversely to be able to immediately remove nested bracket levels
169 # without influencing bracket positions yet to come in the loop.
170 for pos, leveldelta in sorted(itertools.chain(zip(openpos, itertools.repeat(-1)),
171 zip(closepos, itertools.repeat(1))),
172 reverse=True):
173 # the current bracket nesting level
174 level += leveldelta
175 if level < 0:
176 # unbalanced brackets
177 return s
178 if leveldelta == 1 and level == 2:
179 # a closing bracket to cut after
180 endpos = pos+1
181 if leveldelta == -1 and level == 1:
182 # an opening bracket to cut at -> remove
183 r = r[:pos] + r[endpos:]
184 return r
187 class TexResultError(ValueError):
188 "Error raised by :class:`texmessage` parsers."
189 pass
192 class texmessage:
193 """Collection of TeX output parsers.
195 This class is not meant to be instanciated. Instead, it serves as a
196 namespace for TeX output parsers, which are functions receiving a TeX
197 output and returning parsed output.
199 In addition, this class also contains some generator functions (namely
200 :attr:`texmessage.no_file` and :attr:`texmessage.pattern`), which return a
201 function according to the given parameters. They are used to generate some
202 of the parsers in this class and can be used to create others as well.
205 start_pattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
207 @staticmethod
208 def start(msg):
209 r"""Validate TeX/LaTeX startup message including scrollmode test.
211 Example:
212 >>> texmessage.start(r'''
213 ... This is e-TeX (version)
214 ... *! Undefined control sequence.
215 ... <*> \raiseerror
216 ... %
217 ... ''', 0)
221 # check for "This is e-TeX" etc.
222 if not texmessage.start_pattern.search(msg):
223 raise TexResultError("TeX startup failed")
225 # check for \raiseerror -- just to be sure that communication works
226 new = msg.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
227 if msg == new:
228 raise TexResultError("TeX scrollmode check failed")
229 return new
231 @staticmethod
232 def no_file(fileending, qualname=None):
233 "Generator function to ignore the missing file message for fileending."
234 def check(msg):
235 "Ignore the missing {} file message."
236 return msg.replace("No file texput.%s." % fileending, "").replace("No file %s%stexput.%s." % (os.curdir, os.sep, fileending), "")
237 check.__doc__ = check.__doc__.format(fileending)
238 if qualname is not None:
239 check.__qualname__ = qualname
240 return check
242 no_aux = staticmethod(no_file.__func__("aux", "texmessage.no_aux"))
243 no_nav = staticmethod(no_file.__func__("nav", "texmessage.no_nav"))
245 aux_pattern = re.compile(r'\(([^()]+\.aux|"[^"]+\.aux")\)')
246 log_pattern = re.compile(r"Transcript written on .*texput\.log\.", re.DOTALL)
248 @staticmethod
249 def end(msg):
250 "Validate TeX shutdown message."
251 msg = re.sub(texmessage.aux_pattern, "", msg).replace("(see the transcript file for additional information)", "")
253 # check for "Transcript written on ...log."
254 msg, m = remove_pattern(texmessage.log_pattern, msg)
255 if not m:
256 raise TexResultError("TeX logfile message expected")
257 return msg
259 quoted_file_pattern = re.compile(r'\("(?P<filename>[^"]+)".*?\)')
260 file_pattern = re.compile(r'\((?P<filename>[^"][^ )]*).*?\)', re.DOTALL)
262 @staticmethod
263 def load(msg):
264 """Ignore file loading messages.
266 Removes text starting with a round bracket followed by a filename
267 ignoring all further text until the corresponding closing bracket.
268 Quotes and/or line breaks in the filename are handled as needed for TeX
269 output.
271 Without quoting the filename, the necessary removal of line breaks is
272 not well defined and the different possibilities are tested to check
273 whether one solution is ok. The last of the examples below checks this
274 behavior.
276 Examples:
277 >>> texmessage.load(r'''other (text.py) things''', 0)
278 'other things'
279 >>> texmessage.load(r'''other ("text.py") things''', 0)
280 'other things'
281 >>> texmessage.load(r'''other ("tex
282 ... t.py" further (ignored)
283 ... text) things''', 0)
284 'other things'
285 >>> texmessage.load(r'''other (t
286 ... ext
287 ... .py
288 ... fur
289 ... ther (ignored) text) things''', 0)
290 'other things'
293 r = remove_nested_brackets(msg)
294 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
295 while m:
296 if not os.path.isfile(m.group("filename")):
297 return msg
298 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
299 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
300 while m:
301 for filename in itertools.accumulate(m.group("filename").split("\n")):
302 if os.path.isfile(filename):
303 break
304 else:
305 return msg
306 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
307 return r
309 quoted_def_pattern = re.compile(r'\("(?P<filename>[^"]+\.(fd|def))"\)')
310 def_pattern = re.compile(r'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
312 @staticmethod
313 def load_def(msg):
314 "Ignore font definition (``*.fd`` and ``*.def``) loading messages."
315 r = msg
316 for p in [texmessage.quoted_def_pattern, texmessage.def_pattern]:
317 r, m = remove_pattern(p, r)
318 while m:
319 if not os.path.isfile(m.group("filename")):
320 return msg
321 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
322 return r
324 quoted_graphics_pattern = re.compile(r'<"(?P<filename>[^"]+\.eps)">')
325 graphics_pattern = re.compile(r'<(?P<filename>[^"][^>]*\.eps)>')
327 @staticmethod
328 def load_graphics(msg):
329 "Ignore graphics file (``*.eps``) loading messages."
330 r = msg
331 for p in [texmessage.quoted_graphics_pattern, texmessage.graphics_pattern]:
332 r, m = remove_pattern(p, r)
333 while m:
334 if not os.path.isfile(m.group("filename")):
335 return msg
336 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
337 return r
339 @staticmethod
340 def ignore(msg):
341 """Ignore all messages.
343 Should be used as a last resort only. You should write a proper TeX
344 output parser function for the output you observe.
347 return ""
349 @staticmethod
350 def warn(msg):
351 """Warn about all messages.
353 Similar to :attr:`ignore`, but writing a warning to the logger about
354 the TeX output. This is considered to be better when you need to get it
355 working quickly as you will still be prompted about the unresolved
356 output, while the processing continues.
359 if msg:
360 logger.warning("ignoring TeX warnings:\n%s" % indent_text(msg.rstrip()))
361 return ""
363 @staticmethod
364 def pattern(p, warning, qualname=None):
365 "Warn by regular expression pattern matching."
366 def check(msg):
367 "Warn about {}."
368 msg, m = remove_pattern(p, msg, ignore_nl=False)
369 while m:
370 logger.warning("ignoring %s:\n%s" % (warning, m.string[m.start(): m.end()].rstrip()))
371 msg, m = remove_pattern(p, msg, ignore_nl=False)
372 return msg
373 check.__doc__ = check.__doc__.format(warning)
374 if qualname is not None:
375 check.__qualname__ = qualname
376 return check
378 box_warning = staticmethod(pattern.__func__(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE),
379 "overfull/underfull box", qualname="texmessage.box_warning"))
380 font_warning = staticmethod(pattern.__func__(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE),
381 "font substitutions of NFSS", qualname="texmessage.font_warning"))
382 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),
383 "generic package messages", qualname="texmessage.package_warning"))
384 rerun_warning = staticmethod(pattern.__func__(re.compile(r"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re.MULTILINE),
385 "rerun required message", qualname="texmessage.rerun_warning"))
386 nobbl_warning = staticmethod(pattern.__func__(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE),
387 "no-bbl message", qualname="texmessage.nobbl_warning"))
390 ###############################################################################
391 # textattrs
392 ###############################################################################
394 _textattrspreamble = ""
396 class textattr:
397 "a textattr defines a apply method, which modifies a (La)TeX expression"
399 class _localattr: pass
401 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
402 \def\PyXragged{%
403 \leftskip=0pt plus \PyXFlushHAlign fil%
404 \rightskip=0pt plus 1fil%
405 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
406 \parfillskip=0pt%
407 \pretolerance=9999%
408 \tolerance=9999%
409 \parindent=0pt%
410 \hyphenpenalty=9999%
411 \exhyphenpenalty=9999}%
414 class boxhalign(attr.exclusiveattr, textattr, _localattr):
416 def __init__(self, aboxhalign):
417 self.boxhalign = aboxhalign
418 attr.exclusiveattr.__init__(self, boxhalign)
420 def apply(self, expr):
421 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
423 boxhalign.left = boxhalign(0)
424 boxhalign.center = boxhalign(0.5)
425 boxhalign.right = boxhalign(1)
426 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
429 class flushhalign(attr.exclusiveattr, textattr, _localattr):
431 def __init__(self, aflushhalign):
432 self.flushhalign = aflushhalign
433 attr.exclusiveattr.__init__(self, flushhalign)
435 def apply(self, expr):
436 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
438 flushhalign.left = flushhalign(0)
439 flushhalign.center = flushhalign(0.5)
440 flushhalign.right = flushhalign(1)
441 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
444 class halign(boxhalign, flushhalign, _localattr):
446 def __init__(self, aboxhalign, aflushhalign):
447 self.boxhalign = aboxhalign
448 self.flushhalign = aflushhalign
449 attr.exclusiveattr.__init__(self, halign)
451 def apply(self, expr):
452 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
454 halign.left = halign(0, 0)
455 halign.center = halign(0.5, 0.5)
456 halign.right = halign(1, 1)
457 halign.clear = attr.clearclass(halign)
458 halign.boxleft = boxhalign.left
459 halign.boxcenter = boxhalign.center
460 halign.boxright = boxhalign.right
461 halign.flushleft = halign.raggedright = flushhalign.left
462 halign.flushcenter = halign.raggedcenter = flushhalign.center
463 halign.flushright = halign.raggedleft = flushhalign.right
466 class _mathmode(attr.attr, textattr, _localattr):
467 "math mode"
469 def apply(self, expr):
470 return r"$\displaystyle{%s}$" % expr
472 mathmode = _mathmode()
473 clearmathmode = attr.clearclass(_mathmode)
476 class _phantom(attr.attr, textattr, _localattr):
477 "phantom text"
479 def apply(self, expr):
480 return r"\phantom{%s}" % expr
482 phantom = _phantom()
483 clearphantom = attr.clearclass(_phantom)
486 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
488 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
490 top = 1
491 middle = 2
492 bottom = 3
494 def __init__(self, width, baseline=top):
495 self.width = width * 72.27 / (unit.scale["x"] * 72)
496 self.baseline = baseline
497 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
499 def apply(self, expr):
500 if self.baseline == self.top:
501 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
502 elif self.baseline == self.middle:
503 return r"\linewidth=%.5ftruept\setbox\PyXBoxVBox=\hbox{{\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\PyXDimenVBox=0.5\dp\PyXBoxVBox\setbox\PyXBoxVBox=\hbox{{\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}}}\advance\PyXDimenVBox by -0.5\dp\PyXBoxVBox\lower\PyXDimenVBox\box\PyXBoxVBox" % (self.width, expr, expr)
504 elif self.baseline == self.bottom:
505 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
506 else:
507 ValueError("invalid baseline argument")
509 parbox_pt.clear = attr.clearclass(parbox_pt)
511 class parbox(parbox_pt):
513 def __init__(self, width, **kwargs):
514 parbox_pt.__init__(self, unit.topt(width), **kwargs)
516 parbox.clear = parbox_pt.clear
519 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
521 class valign(attr.sortbeforeexclusiveattr, textattr):
523 def __init__(self, avalign):
524 self.valign = avalign
525 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
527 def apply(self, expr):
528 return r"\setbox\PyXBoxVAlign=\hbox{{%s}}\PyXDimenVAlign=%.5f\ht\PyXBoxVAlign\advance\PyXDimenVAlign by -%.5f\dp\PyXBoxVAlign\lower\PyXDimenVAlign\box\PyXBoxVAlign" % (expr, 1-self.valign, self.valign)
530 valign.top = valign(0)
531 valign.middle = valign(0.5)
532 valign.bottom = valign(1)
533 valign.clear = valign.baseline = attr.clearclass(valign)
536 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
538 class _vshift(attr.sortbeforeattr, textattr):
540 def __init__(self):
541 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
543 def apply(self, expr):
544 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
546 class vshift(_vshift):
547 "vertical down shift by a fraction of a character height"
549 def __init__(self, lowerratio, heightstr="0"):
550 _vshift.__init__(self)
551 self.lowerratio = lowerratio
552 self.heightstr = heightstr
554 def setheightexpr(self):
555 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
557 class _vshiftmathaxis(_vshift):
558 "vertical down shift by the height of the math axis"
560 def setheightexpr(self):
561 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
564 vshift.bottomzero = vshift(0)
565 vshift.middlezero = vshift(0.5)
566 vshift.topzero = vshift(1)
567 vshift.mathaxis = _vshiftmathaxis()
568 vshift.clear = attr.clearclass(_vshift)
571 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
572 None, "tiny", "scriptsize", "footnotesize", "small"]
574 class size(attr.sortbeforeattr, textattr):
575 "font size"
577 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
578 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
579 raise ValueError("either specify sizeindex or sizename")
580 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
581 if sizeindex is not None:
582 if sizeindex >= 0 and sizeindex < sizelist.index(None):
583 self.size = sizelist[sizeindex]
584 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
585 self.size = sizelist[sizeindex]
586 else:
587 raise IndexError("index out of sizelist range")
588 else:
589 self.size = sizename
591 def apply(self, expr):
592 return r"\%s{}%s" % (self.size, expr)
594 size.tiny = size(-4)
595 size.scriptsize = size.script = size(-3)
596 size.footnotesize = size.footnote = size(-2)
597 size.small = size(-1)
598 size.normalsize = size.normal = size(0)
599 size.large = size(1)
600 size.Large = size(2)
601 size.LARGE = size(3)
602 size.huge = size(4)
603 size.Huge = size(5)
604 size.clear = attr.clearclass(size)
607 ###############################################################################
608 # texrunner
609 ###############################################################################
612 class MonitorOutput(threading.Thread):
614 def __init__(self, name, output):
615 """Deadlock-safe output stream reader and monitor.
617 This method sets up a thread to continously read lines from a stream.
618 By that a deadlock due to a full pipe is prevented. In addition, the
619 stream content can be monitored for containing a certain string (see
620 :meth:`expect` and :meth:`wait`) and return all the collected output
621 (see :meth:`read`).
623 :param string name: name to be used while logging in :meth:`wait` and
624 :meth:`done`
625 :param file output: output stream
628 self.output = output
629 self._expect = queue.Queue(1)
630 self._received = threading.Event()
631 self._output = queue.Queue()
632 threading.Thread.__init__(self, name=name)
633 self.daemon = True
634 self.start()
636 def expect(self, s):
637 """Expect a string on a **single** line in the output.
639 This method must be called **before** the output occurs, i.e. before
640 the input is written to the TeX/LaTeX process.
642 :param s: expected string or ``None`` if output is expected to become
643 empty
644 :type s: str or None
647 self._expect.put_nowait(s)
649 def read(self):
650 """Read all output collected since its previous call.
652 The output reading should be synchronized by the :meth:`expect`
653 and :meth:`wait` methods.
655 :returns: collected output from the stream
656 :rtype: str
659 l = []
660 try:
661 while True:
662 l.append(self._output.get_nowait())
663 except queue.Empty:
664 pass
665 return "".join(l).replace("\r\n", "\n").replace("\r", "\n")
667 def _wait(self, waiter, checker):
668 """Helper method to implement :meth:`wait` and :meth:`done`.
670 Waits for an event using the *waiter* and *checker* functions while
671 providing user feedback to the ``pyx``-logger using the warning level
672 according to the ``wait`` and ``showwait`` from the ``text`` section of
673 the pyx :mod:`config`.
675 :param function waiter: callback to wait for (the function gets called
676 with a timeout parameter)
677 :param function checker: callback returing ``True`` if
678 waiting was successful
679 :returns: ``True`` when wait was successful
680 :rtype: bool
683 wait = config.getint("text", "wait", 60)
684 showwait = config.getint("text", "showwait", 5)
685 if showwait:
686 waited = 0
687 hasevent = False
688 while waited < wait and not hasevent:
689 if wait - waited > showwait:
690 waiter(showwait)
691 waited += showwait
692 else:
693 waiter(wait - waited)
694 waited += wait - waited
695 hasevent = checker()
696 if not hasevent:
697 if waited < wait:
698 logger.warning("Still waiting for {} "
699 "after {} (of {}) seconds..."
700 .format(self.name, waited, wait))
701 else:
702 logger.warning("The timeout of {} seconds expired "
703 "and {} did not respond."
704 .format(waited, self.name))
705 return hasevent
706 else:
707 waiter(wait)
708 return checker()
710 def wait(self):
711 """Wait for the expected output to happen.
713 Waits either until a line containing the string set by the previous
714 :meth:`expect` call is found, or a timeout occurs.
716 :returns: ``True`` when the expected string was found
717 :rtype: bool
720 r = self._wait(self._received.wait, self._received.isSet)
721 if r:
722 self._received.clear()
723 return r
725 def done(self):
726 """Waits until the output becomes empty.
728 Waits either until the output becomes empty, or a timeout occurs.
729 The generated output can still be catched by :meth:`read` after
730 :meth:`done` was successful.
732 In the proper workflow :meth:`expect` should be called with ``None``
733 before the output completes, as otherwise a ``ValueError`` is raised
734 in the :meth:`run`.
736 :returns: ``True`` when the output has become empty
737 :rtype: bool
740 return self._wait(self.join, lambda self=self: not self.is_alive())
742 def _readline(self):
743 """Read a line from the output.
745 To be used **inside** the thread routine only.
747 :returns: one line of the output as a string
748 :rtype: str
751 while True:
752 try:
753 return self.output.readline()
754 except IOError as e:
755 if e.errno != errno.EINTR:
756 raise
758 def run(self):
759 """Thread routine.
761 **Not** to be called from outside.
763 :raises ValueError: output becomes empty while some string is expected
766 expect = None
767 while True:
768 line = self._readline()
769 if expect is None:
770 try:
771 expect = self._expect.get_nowait()
772 except queue.Empty:
773 pass
774 if not line:
775 break
776 self._output.put(line)
777 if expect is not None:
778 found = line.find(expect)
779 if found != -1:
780 self._received.set()
781 expect = None
782 self.output.close()
783 if expect is not None:
784 raise ValueError("{} finished unexpectedly".format(self.name))
787 class textbox(box.rect, baseclasses.canvasitem):
788 """basically a box.rect, but it contains a text created by the texrunner
789 - texrunner._text and texrunner.text return such an object
790 - _textbox instances can be inserted into a canvas
791 - the output is contained in a page of the dvifile available thru the texrunner"""
792 # TODO: shouldn't all boxes become canvases? how about inserts then?
794 def __init__(self, x, y, left, right, height, depth, do_finish, fontmap, singlecharmode, attrs):
796 - do_finish is a method to be called to get the dvicanvas
797 (e.g. the do_finish calls the setdvicanvas method)
798 - attrs are fillstyles"""
799 self.left = left
800 self.right = right
801 self.width = left + right
802 self.height = height
803 self.depth = depth
804 self.do_finish = do_finish
805 self.fontmap = fontmap
806 self.singlecharmode = singlecharmode
807 self.attrs = attrs
809 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
810 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
812 self.dvicanvas = None
813 self.insertdvicanvas = False
815 def transform(self, *trafos):
816 box.rect.transform(self, *trafos)
817 if self.dvicanvas:
818 for trafo in trafos:
819 self.dvicanvas.trafo = trafo * self.dvicanvas.trafo
821 def readdvipage(self, dvifile, page):
822 self.dvicanvas = dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0],
823 fontmap=self.fontmap, singlecharmode=self.singlecharmode, attrs=[self.texttrafo] + self.attrs)
825 def marker(self, marker):
826 self.do_finish()
827 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
829 def textpath(self):
830 self.do_finish()
831 textpath = path.path()
832 for item in self.dvicanvas.items:
833 textpath += item.textpath()
834 return textpath.transformed(self.texttrafo)
836 def processPS(self, file, writer, context, registry, bbox):
837 self.do_finish()
838 abbox = bboxmodule.empty()
839 self.dvicanvas.processPS(file, writer, context, registry, abbox)
840 bbox += box.rect.bbox(self)
842 def processPDF(self, file, writer, context, registry, bbox):
843 self.do_finish()
844 abbox = bboxmodule.empty()
845 self.dvicanvas.processPDF(file, writer, context, registry, abbox)
846 bbox += box.rect.bbox(self)
849 class _marker:
850 pass
853 class errordetail:
854 "Constants defining the verbosity of the :exc:`TexResultError`."
855 none = 0 #: Without any input and output.
856 default = 1 #: Input and parsed output shortend to 5 lines.
857 full = 2 #: Full input and unparsed as well as parsed output.
860 class Tee(object):
862 def __init__(self, *files):
863 self.files = files
865 def write(self, data):
866 for file in self.files:
867 file.write(data)
869 def flush(self):
870 for file in self.files:
871 file.flush()
873 def close(self):
874 for file in self.files:
875 file.close()
877 # The texrunner state represents the next (or current) execute state.
878 STATE_START, STATE_PREAMBLE, STATE_TYPESET, STATE_DONE = range(4)
879 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:")
880 dvi_pattern = re.compile(r"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re.DOTALL)
882 class TexDoneError(Exception):
883 pass
886 class SingleRunner:
888 texmessages_start_default = [texmessage.start]
889 #: default :class:`texmessage` parsers for interpreter startup
890 texmessages_start_default = [texmessage.start]
891 #: default :class:`texmessage` parsers for interpreter shutdown
892 texmessages_end_default = [texmessage.end, texmessage.font_warning, texmessage.rerun_warning, texmessage.nobbl_warning]
893 #: default :class:`texmessage` parsers for preamble output
894 texmessages_preamble_default = [texmessage.load]
895 #: default :class:`texmessage` parsers for typeset output
896 texmessages_run_default = [texmessage.font_warning, texmessage.box_warning, texmessage.package_warning,
897 texmessage.load_def, texmessage.load_graphics]
899 def __init__(self, executable,
900 texenc="ascii",
901 usefiles=[],
902 texipc=config.getboolean("text", "texipc", 0),
903 copyinput=None,
904 dvitype=False,
905 errordetail=errordetail.default,
906 texmessages_start=[],
907 texmessages_end=[],
908 texmessages_preamble=[],
909 texmessages_run=[]):
910 """Base class for the TeX interface.
912 .. note:: This class cannot be used directly. It is the base class for
913 all texrunners and provides most of the implementation.
914 Still, to the end user the parameters except for *executable*
915 are important, as they are preserved in derived classes
916 usually.
918 :param str executable: command to start the TeX interpreter
919 :param str texenc: encoding to use in the communication with the TeX
920 interpreter
921 :param usefiles: list of supplementary files
922 :type usefiles: list of str
923 :param bool texipc: :ref:`texipc` flag.
924 :param copyinput: filename or file to be used to store a copy of all
925 the input passed to the TeX interpreter
926 :type copyinput: None or str or file
927 :param bool dvitype: flag to turn on dvitype-like output
928 :param errordetail: verbosity of the :exc:`TexResultError`
929 :type errordetail: :class:`errordetail`
930 :param texmessages_start: additional message parsers at interpreter
931 startup
932 :type texmessages_start: list of :class:`texmessage` parsers
933 :param texmessages_end: additional message parsers at interpreter
934 shutdown
935 :type texmessages_end: list of :class:`texmessage` parsers
936 :param texmessages_preamble: additional message parsers for preamble
937 output
938 :type texmessages_preamble: list of :class:`texmessage` parsers
939 :param texmessages_run: additional message parsers for typset output
940 :type texmessages_run: list of :class:`texmessage` parsers
942 self.executable = executable
943 self.texenc = texenc
944 self.usefiles = usefiles
945 self.texipc = texipc
946 self.copyinput = copyinput
947 self.dvitype = dvitype
948 self.errordetail = errordetail
949 self.texmessages_start = texmessages_start
950 self.texmessages_end = texmessages_end
951 self.texmessages_preamble = texmessages_preamble
952 self.texmessages_run = texmessages_run
954 self.state = STATE_START
955 self.executeid = 0
956 self.page = 0
958 self.needdvitextboxes = [] # when texipc-mode off
959 self.dvifile = None
961 self.tmpdir = None
963 def _cleanup(self):
964 """Clean-up TeX interpreter and tmp directory.
966 This funtion is hooked up in atexit to quit the TeX interpreter, to
967 save contents of usefiles, and to remove the temporary directory.
970 try:
971 if self.state > STATE_START:
972 if self.state < STATE_DONE:
973 self.do_finish()
974 if self.state < STATE_DONE: # cleanup while TeX is still running?
975 self.texoutput.expect(None)
976 self.force_done()
977 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)),
978 (self.popen.terminate, "Failed, too. Trying by kill signal now ..."),
979 (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))]:
981 if self.texoutput.done():
982 break
983 logger.warning(msg)
984 for usefile in self.usefiles:
985 extpos = usefile.rfind(".")
986 try:
987 os.rename(os.path.join(self.tmpdir, "texput" + usefile[extpos:]), usefile)
988 except EnvironmentError:
989 logger.warning("Could not save '{}'.".format(usefile))
990 if os.path.isfile(usefile):
991 try:
992 os.unlink(usefile)
993 except EnvironmentError:
994 logger.warning("Failed to remove spurious file '{}'.".format(usefile))
995 finally:
996 shutil.rmtree(self.tmpdir, ignore_errors=True)
998 def _execute(self, expr, texmessages, oldstate, newstate):
999 """executes expr within TeX/LaTeX"""
1000 assert STATE_PREAMBLE <= oldstate <= STATE_TYPESET
1001 assert oldstate == self.state
1002 assert newstate >= oldstate
1003 if newstate == STATE_DONE:
1004 self.texoutput.expect(None)
1005 self.texinput.write(expr)
1006 else:
1007 if oldstate == newstate == STATE_TYPESET:
1008 self.page += 1
1009 expr = "\\ProcessPyXBox{%s%%\n}{%i}" % (expr, self.page)
1010 self.executeid += 1
1011 self.texoutput.expect("PyXInputMarker:executeid=%i:" % self.executeid)
1012 expr += "%%\n\\PyXInput{%i}%%\n" % self.executeid
1013 self.texinput.write(expr)
1014 self.texinput.flush()
1015 self.state = newstate
1016 if newstate == STATE_DONE:
1017 wait_ok = self.texoutput.done()
1018 else:
1019 wait_ok = self.texoutput.wait()
1020 try:
1021 parsed = unparsed = self.texoutput.read()
1022 if not wait_ok:
1023 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1024 if newstate != STATE_DONE:
1025 parsed, m = remove_string("PyXInputMarker:executeid=%s:" % self.executeid, parsed)
1026 if not m:
1027 raise TexResultError("PyXInputMarker expected")
1028 if oldstate == newstate == STATE_TYPESET:
1029 parsed, m = remove_pattern(PyXBoxPattern, parsed, ignore_nl=False)
1030 if not m:
1031 raise TexResultError("PyXBox expected")
1032 if m.group("page") != str(self.page):
1033 raise TexResultError("Wrong page number in PyXBox")
1034 extent = [float(x)*72/72.27*unit.x_pt for x in m.group("lt", "rt", "ht", "dp")]
1035 parsed, m = remove_string("[80.121.88.%s]" % self.page, parsed)
1036 if not m:
1037 raise TexResultError("PyXPageOutMarker expected")
1038 else:
1039 # check for "Output written on ...dvi (1 page, 220 bytes)."
1040 if self.page:
1041 parsed, m = remove_pattern(dvi_pattern, parsed)
1042 if not m:
1043 raise TexResultError("TeX dvifile messages expected")
1044 if m.group("page") != str(self.page):
1045 raise TexResultError("wrong number of pages reported")
1046 else:
1047 parsed, m = remove_string("No pages of output.", parsed)
1048 if not m:
1049 raise TexResultError("no dvifile expected")
1051 for t in texmessages:
1052 parsed = t(parsed)
1053 if parsed.replace(r"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1054 raise TexResultError("unhandled TeX response (might be an error)")
1055 except TexResultError as e:
1056 if self.errordetail > errordetail.none:
1057 def add(msg): e.args = (e.args[0] + msg,)
1058 add("\nThe expression passed to TeX was:\n{}".format(indent_text(expr.rstrip())))
1059 if self.errordetail == errordetail.full:
1060 add("\nThe return message from TeX was:\n{}".format(indent_text(unparsed.rstrip())))
1061 if self.errordetail == errordetail.default:
1062 if parsed.count('\n') > 6:
1063 parsed = "\n".join(parsed.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1064 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(indent_text(parsed.rstrip())))
1065 raise e
1066 if oldstate == newstate == STATE_TYPESET:
1067 return extent
1069 def do_start(self):
1070 assert self.state == STATE_START
1071 self.state = STATE_PREAMBLE
1073 if self.tmpdir is None:
1074 self.tmpdir = tempfile.mkdtemp()
1075 atexit.register(self._cleanup)
1076 for usefile in self.usefiles:
1077 extpos = usefile.rfind(".")
1078 try:
1079 os.rename(usefile, os.path.join(self.tmpdir, "texput" + usefile[extpos:]))
1080 except OSError:
1081 pass
1082 cmd = [self.executable, '--output-directory', self.tmpdir]
1083 if self.texipc:
1084 cmd.append("--ipc")
1085 self.popen = config.Popen(cmd, stdin=config.PIPE, stdout=config.PIPE, stderr=config.STDOUT, bufsize=0)
1086 self.texinput = io.TextIOWrapper(self.popen.stdin, encoding=self.texenc)
1087 if self.copyinput:
1088 try:
1089 self.copyinput.write
1090 except AttributeError:
1091 self.texinput = Tee(open(self.copyinput, "w", encoding=self.texenc), self.texinput)
1092 else:
1093 self.texinput = Tee(self.copyinput, self.texinput)
1094 self.texoutput = MonitorOutput(self.name, io.TextIOWrapper(self.popen.stdout, encoding=self.texenc))
1095 self._execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1096 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1097 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1098 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1099 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1100 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1101 "\\newdimen\\PyXDimenHAlignRT%\n" +
1102 _textattrspreamble + # insert preambles for textattrs macros
1103 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1104 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1105 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1106 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1107 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1108 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1109 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1110 "lt=\\the\\PyXDimenHAlignLT,"
1111 "rt=\\the\\PyXDimenHAlignRT,"
1112 "ht=\\the\\ht\\PyXBox,"
1113 "dp=\\the\\dp\\PyXBox:}%\n"
1114 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1115 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1116 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1117 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1118 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1119 self.texmessages_start_default + self.texmessages_start, STATE_PREAMBLE, STATE_PREAMBLE)
1121 def do_preamble(self, expr, texmessages):
1122 if self.state < STATE_PREAMBLE:
1123 self.do_start()
1124 self._execute(expr, texmessages, STATE_PREAMBLE, STATE_PREAMBLE)
1126 def do_typeset(self, expr, texmessages):
1127 if self.state < STATE_PREAMBLE:
1128 self.do_start()
1129 if self.state < STATE_TYPESET:
1130 self.go_typeset()
1131 return self._execute(expr, texmessages, STATE_TYPESET, STATE_TYPESET)
1133 def do_finish(self):
1134 if self.state == STATE_DONE:
1135 return
1136 if self.state < STATE_TYPESET:
1137 self.go_typeset()
1138 self.go_finish()
1139 self.texinput.close() # close the input queue and
1140 self.texoutput.done() # wait for finish of the output
1142 if not self.texipc:
1143 dvifilename = os.path.join(self.tmpdir, "texput.dvi")
1144 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvitype)
1145 page = 1
1146 for box in self.needdvitextboxes:
1147 box.readdvipage(self.dvifile, page)
1148 page += 1
1149 if self.dvifile.readpage(None) is not None:
1150 raise ValueError("end of dvifile expected but further pages follow")
1152 def preamble(self, expr, texmessages=[]):
1153 r"""Execute a preamble.
1155 :param str expr: expression to be executed
1156 :param texmessages: additional message parsers
1157 :type texmessages: list of :class:`texmessage` parsers
1159 Preambles must not generate output, but are used to load files, perform
1160 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1161 before ``\begin{document}``. The method can be called multiple times,
1162 but only prior to :meth:`SingleRunner.text` and
1163 :meth:`SingleRunner.text_pt`.
1166 texmessages = self.texmessages_preamble_default + self.texmessages_preamble + texmessages
1167 self.do_preamble(expr, texmessages)
1169 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1170 """create text by passing expr to TeX/LaTeX
1171 - returns a textbox containing the result from running expr thru TeX/LaTeX
1172 - the box center is set to x, y
1173 - *args may contain attr parameters, namely:
1174 - textattr instances
1175 - texmessage instances
1176 - trafo._trafo instances
1177 - style.fillstyle instances"""
1178 if self.state == STATE_DONE:
1179 raise TexDoneError("typesetting process was terminated already")
1180 textattrs = attr.mergeattrs(textattrs) # perform cleans
1181 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1182 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1183 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1184 textattrs = attr.getattrs(textattrs, [textattr])
1185 for ta in textattrs[::-1]:
1186 expr = ta.apply(expr)
1187 first = self.state < STATE_TYPESET
1188 left, right, height, depth = self.do_typeset(expr, self.texmessages_run_default + self.texmessages_run + texmessages)
1189 if self.texipc and first:
1190 self.dvifile = dvifile.DVIfile(os.path.join(self.tmpdir, "texput.dvi"), debug=self.dvitype)
1191 box = textbox(x, y, left, right, height, depth, self.do_finish, fontmap, singlecharmode, fillstyles)
1192 for t in trafos:
1193 box.reltransform(t) # TODO: should trafos really use reltransform???
1194 # this is quite different from what we do elsewhere!!!
1195 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1196 if self.texipc:
1197 box.readdvipage(self.dvifile, self.page)
1198 else:
1199 self.needdvitextboxes.append(box)
1200 return box
1202 def text_pt(self, x, y, expr, *args, **kwargs):
1203 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1206 class SingleTexRunner(SingleRunner):
1208 def __init__(self, executable=config.get("text", "tex", "tex"), lfs="10pt", **kwargs):
1209 super().__init__(executable=executable, **kwargs)
1210 self.lfs = lfs
1211 self.name = "TeX"
1213 def go_typeset(self):
1214 assert self.state == STATE_PREAMBLE
1215 self.state = STATE_TYPESET
1217 def go_finish(self):
1218 self._execute("\\end%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1220 def force_done(self):
1221 self.texinput.write("\n\\end\n")
1223 def do_start(self):
1224 super().do_start()
1225 if self.lfs:
1226 if not self.lfs.endswith(".lfs"):
1227 self.lfs = "%s.lfs" % self.lfs
1228 with config.open(self.lfs, []) as lfsfile:
1229 lfsdef = lfsfile.read().decode("ascii")
1230 self._execute(lfsdef, [], STATE_PREAMBLE, STATE_PREAMBLE)
1231 self._execute("\\normalsize%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1232 self._execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1235 class SingleLatexRunner(SingleRunner):
1237 texmessages_docclass_default = [texmessage.load]
1238 texmessages_begindoc_default = [texmessage.load, texmessage.no_aux]
1240 def __init__(self, executable=config.get("text", "latex", "latex"),
1241 docclass="article", docopt=None, pyxgraphics=True,
1242 texmessages_docclass=[], texmessages_begindoc=[], **kwargs):
1243 super().__init__(executable=executable, **kwargs)
1244 self.docclass = docclass
1245 self.docopt = docopt
1246 self.pyxgraphics = pyxgraphics
1247 self.texmessages_docclass = texmessages_docclass
1248 self.texmessages_begindoc = texmessages_begindoc
1249 self.name = "LaTeX"
1251 def go_typeset(self):
1252 self._execute("\\begin{document}", self.texmessages_begindoc_default + self.texmessages_begindoc, STATE_PREAMBLE, STATE_TYPESET)
1254 def go_finish(self):
1255 self._execute("\\end{document}%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1257 def force_done(self):
1258 self.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1260 def do_start(self):
1261 super().do_start()
1262 if self.pyxgraphics:
1263 with config.open("pyx.def", []) as source, open(os.path.join(self.tmpdir, "pyx.def"), "wb") as dest:
1264 dest.write(source.read())
1265 self._execute("\\makeatletter%\n"
1266 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1267 "\\def\\ProcessOptions{%\n"
1268 "\\def\\Gin@driver{" + self.tmpdir.replace(os.sep, "/") + "/pyx.def}%\n"
1269 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1270 "\\saveProcessOptions}%\n"
1271 "\\makeatother",
1272 [], STATE_PREAMBLE, STATE_PREAMBLE)
1273 if self.docopt is not None:
1274 self._execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
1275 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1276 else:
1277 self._execute("\\documentclass{%s}" % self.docclass,
1278 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1281 def reset_for_tex_done(f):
1282 @functools.wraps(f)
1283 def wrapped(self, *args, **kwargs):
1284 try:
1285 return f(self, *args, **kwargs)
1286 except TexDoneError:
1287 self.reset(reinit=True)
1288 return f(self, *args, **kwargs)
1289 return wrapped
1292 class MultiRunner:
1294 def __init__(self, cls, *args, **kwargs):
1295 self.cls = cls
1296 self.args = args
1297 self.kwargs = kwargs
1298 self.reset()
1300 def preamble(self, expr, texmessages=[]):
1301 self.preambles.append((expr, texmessages))
1302 self.instance.preamble(expr, texmessages)
1304 @reset_for_tex_done
1305 def text_pt(self, *args, **kwargs):
1306 return self.instance.text_pt(*args, **kwargs)
1308 @reset_for_tex_done
1309 def text(self, *args, **kwargs):
1310 return self.instance.text(*args, **kwargs)
1312 def reset(self, reinit=False):
1313 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1314 self.instance = self.cls(*self.args, **self.kwargs)
1315 if reinit:
1316 for expr, texmessages in self.preambles:
1317 self.instance.preamble(expr, texmessages)
1318 else:
1319 self.preambles = []
1322 class TexRunner(MultiRunner):
1324 def __init__(self, **kwargs):
1325 super().__init__(SingleTexRunner, **kwargs)
1328 class LatexRunner(MultiRunner):
1330 def __init__(self, **kwargs):
1331 super().__init__(SingleLatexRunner, **kwargs)
1334 # old, deprecated names:
1335 texrunner = TexRunner
1336 latexrunner = LatexRunner
1338 def set(mode="tex", **kwargs):
1339 # note: defaulttexrunner is deprecated
1340 global default_runner, defaulttexrunner, reset, preamble, text, text_pt
1341 mode = mode.lower()
1342 if mode == "tex":
1343 default_runner = defaulttexrunner = TexRunner(**kwargs)
1344 elif mode == "latex":
1345 default_runner = defaulttexrunner = LatexRunner(**kwargs)
1346 else:
1347 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1348 reset = default_runner.reset
1349 preamble = default_runner.preamble
1350 text = default_runner.text
1351 text_pt = default_runner.text_pt
1353 set()
1355 def escapestring(s, replace={" ": "~",
1356 "$": "\\$",
1357 "&": "\\&",
1358 "#": "\\#",
1359 "_": "\\_",
1360 "%": "\\%",
1361 "^": "\\string^",
1362 "~": "\\string~",
1363 "<": "{$<$}",
1364 ">": "{$>$}",
1365 "{": "{$\{$}",
1366 "}": "{$\}$}",
1367 "\\": "{$\setminus$}",
1368 "|": "{$\mid$}"}):
1369 "escape all ascii characters such that they are printable by TeX/LaTeX"
1370 i = 0
1371 while i < len(s):
1372 if not 32 <= ord(s[i]) < 127:
1373 raise ValueError("escapestring function handles ascii strings only")
1374 c = s[i]
1375 try:
1376 r = replace[c]
1377 except KeyError:
1378 i += 1
1379 else:
1380 s = s[:i] + r + s[i+1:]
1381 i += len(r)
1382 return s