logger.warning instead of logger.warn (as it triggers a warning otherwise, what a...
[PyX.git] / pyx / text.py
blobbc6a383f56c186f56f461a1bedca52994fc20b17
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, canvas, 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): pass
184 class texmessage:
186 start_pattern = re.compile(r"This is [-0-9a-zA-Z\s_]*TeX")
188 @staticmethod
189 def start(msg, page):
190 r"""Message parser to check for proper TeX startup.
192 Example:
193 >>> texmessage.start(r'''
194 ... This is e-TeX (version)
195 ... *! Undefined control sequence.
196 ... <*> \raiseerror
197 ... %
198 ... ''', 0)
202 # check for "This is e-TeX" etc.
203 if not texmessage.start_pattern.search(msg):
204 raise TexResultError("TeX startup failed")
206 # check for \raiseerror -- just to be sure that communication works
207 new = msg.split("*! Undefined control sequence.\n<*> \\raiseerror\n %\n", 1)[-1]
208 if msg == new:
209 raise TexResultError("TeX scrollmode check failed")
210 return new
212 @staticmethod
213 def no_file(fileending):
214 "Message parser generator for missing file with given fileending."
215 def check(msg, page):
216 "Message parser for missing {} file."
217 return msg.replace("No file texput.%s." % fileending, "").replace("No file %s%stexput.%s." % (os.curdir, os.sep, fileending), "")
218 check.__doc__ = check.__doc__.format(fileending)
219 return check
221 no_aux = no_file.__func__("aux")
222 no_nav = no_file.__func__("nav")
224 aux_pattern = re.compile(r'\(([^()]+\.aux|"[^"]+\.aux")\)')
225 dvi_pattern = re.compile(r"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re.DOTALL)
226 log_pattern = re.compile(r"Transcript written on .*texput\.log\.", re.DOTALL)
228 @staticmethod
229 def end(msg, page):
230 "Message parser to check for proper TeX shutdown."
231 msg = re.sub(texmessage.aux_pattern, "", msg).replace("(see the transcript file for additional information)", "")
233 # check for "Output written on ...dvi (1 page, 220 bytes)."
234 if page:
235 msg, m = remove_pattern(texmessage.dvi_pattern, msg)
236 if not m:
237 raise TexResultError("TeX dvifile messages expected")
238 if m.group("page") != str(page):
239 raise TexResultError("wrong number of pages reported")
240 else:
241 msg, m = remove_string("No pages of output.", msg)
242 if not m:
243 raise TexResultError("no dvifile expected")
245 # check for "Transcript written on ...log."
246 msg, m = remove_pattern(texmessage.log_pattern, msg)
247 if not m:
248 raise TexResultError("TeX logfile message expected")
249 return msg
251 quoted_file_pattern = re.compile(r'\("(?P<filename>[^"]+)".*?\)')
252 file_pattern = re.compile(r'\((?P<filename>[^"][^ )]*).*?\)', re.DOTALL)
254 @staticmethod
255 def load(msg, page):
256 """Message parser for loading of files.
258 Removes text starting with a round bracket followed by a filename
259 ignoring all further text until the corresponding closing bracket.
260 Quotes and/or line breaks in the filename are handled as needed to read
261 TeX output.
263 Without quoting the filename, the necessary removal of line breaks is
264 not well defined and the different possibilities are tested to check
265 whether one solution is ok. The last of the examples below checks this
266 behavior.
268 Examples:
269 >>> texmessage.load(r'''other (text.py) things''', 0)
270 'other things'
271 >>> texmessage.load(r'''other ("text.py") things''', 0)
272 'other things'
273 >>> texmessage.load(r'''other ("tex
274 ... t.py" further (ignored)
275 ... text) things''', 0)
276 'other things'
277 >>> texmessage.load(r'''other (t
278 ... ext
279 ... .py
280 ... fur
281 ... ther (ignored) text) things''', 0)
282 'other things'
285 r = remove_nested_brackets(msg)
286 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
287 while m:
288 if not os.path.isfile(m.group("filename")):
289 return msg
290 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
291 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
292 while m:
293 for filename in itertools.accumulate(m.group("filename").split("\n")):
294 if os.path.isfile(filename):
295 break
296 else:
297 return msg
298 r, m = remove_pattern(texmessage.file_pattern, r, ignore_nl=False)
299 return r
301 quoted_def_pattern = re.compile(r'\("(?P<filename>[^"]+\.(fd|def))"\)')
302 def_pattern = re.compile(r'\((?P<filename>[^"][^ )]*\.(fd|def))\)')
304 @staticmethod
305 def load_def(msg, page):
306 "Message parser for loading of font definition files."
307 r = msg
308 for p in [texmessage.quoted_def_pattern, texmessage.def_pattern]:
309 r, m = remove_pattern(p, r)
310 while m:
311 if not os.path.isfile(m.group("filename")):
312 return msg
313 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
314 return r
316 quoted_graphics_pattern = re.compile(r'<"(?P<filename>[^"]+\.eps)">')
317 graphics_pattern = re.compile(r'<(?P<filename>[^"][^>]*\.eps)>')
319 @staticmethod
320 def load_graphics(msg, page):
321 "Message parser for loading of graphics files."
322 r = msg
323 for p in [texmessage.quoted_graphics_pattern, texmessage.graphics_pattern]:
324 r, m = remove_pattern(p, r)
325 while m:
326 if not os.path.isfile(m.group("filename")):
327 return msg
328 r, m = remove_pattern(texmessage.quoted_file_pattern, r)
329 return r
331 @staticmethod
332 def ignore(msg, page):
333 """Message parser to ignore all output.
335 Should be used as a last resort only. You should write a proper message
336 parser checking for the output you observe.
339 return ""
341 @staticmethod
342 def warn(msg, page):
343 """Message parser to warn about all output.
345 Similar to the :meth:`ignore` message parser, but writing a warning to
346 the logger about the message. This is considered to be better when you
347 need to get it working quickly as you will still be prompted about the
348 unresolved message, while the processing continues.
351 if msg:
352 logger.warning("ignoring TeX warnings:\n%s" % textwrap.indent(msg.rstrip(), " "))
353 return ""
355 @staticmethod
356 def pattern(p, warning):
357 "Message parser generator for pattern matching."
358 def check(msg, page):
359 "Message parser for {}."
360 msg, m = remove_pattern(p, msg, ignore_nl=False)
361 while m:
362 logger.warning("ignoring %s:\n%s" % (warning, m.string[m.start(): m.end()].rstrip()))
363 msg, m = remove_pattern(p, msg, ignore_nl=False)
364 return msg
365 check.__doc__ = check.__doc__.format(warning)
366 return check
368 box_warning = pattern.__func__(re.compile(r"^(Overfull|Underfull) \\[hv]box.*$(\n^..*$)*\n^$\n", re.MULTILINE), "overfull/underfull box warning")
369 font_warning = pattern.__func__(re.compile(r"^LaTeX Font Warning: .*$(\n^\(Font\).*$)*", re.MULTILINE), "font warning")
370 package_warning = pattern.__func__(re.compile(r"^package\s+(?P<packagename>\S+)\s+warning\s*:[^\n]+(?:\n\(?(?P=packagename)\)?[^\n]*)*", re.MULTILINE | re.IGNORECASE), "generic package warning")
371 rerun_warning = pattern.__func__(re.compile(r"^(LaTeX Warning: Label\(s\) may have changed\. Rerun to get cross-references right\s*\.)$", re.MULTILINE), "rerun warning")
372 nobbl_warning = pattern.__func__(re.compile(r"^[\s\*]*(No file .*\.bbl.)\s*", re.MULTILINE), "no-bbl warning")
376 ###############################################################################
377 # textattrs
378 ###############################################################################
380 _textattrspreamble = ""
382 class textattr:
383 "a textattr defines a apply method, which modifies a (La)TeX expression"
385 class _localattr: pass
387 _textattrspreamble += r"""\gdef\PyXFlushHAlign{0}%
388 \def\PyXragged{%
389 \leftskip=0pt plus \PyXFlushHAlign fil%
390 \rightskip=0pt plus 1fil%
391 \advance\rightskip0pt plus -\PyXFlushHAlign fil%
392 \parfillskip=0pt%
393 \pretolerance=9999%
394 \tolerance=9999%
395 \parindent=0pt%
396 \hyphenpenalty=9999%
397 \exhyphenpenalty=9999}%
400 class boxhalign(attr.exclusiveattr, textattr, _localattr):
402 def __init__(self, aboxhalign):
403 self.boxhalign = aboxhalign
404 attr.exclusiveattr.__init__(self, boxhalign)
406 def apply(self, expr):
407 return r"\gdef\PyXBoxHAlign{%.5f}%s" % (self.boxhalign, expr)
409 boxhalign.left = boxhalign(0)
410 boxhalign.center = boxhalign(0.5)
411 boxhalign.right = boxhalign(1)
412 # boxhalign.clear = attr.clearclass(boxhalign) # we can't defined a clearclass for boxhalign since it can't clear a halign's boxhalign
415 class flushhalign(attr.exclusiveattr, textattr, _localattr):
417 def __init__(self, aflushhalign):
418 self.flushhalign = aflushhalign
419 attr.exclusiveattr.__init__(self, flushhalign)
421 def apply(self, expr):
422 return r"\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.flushhalign, expr)
424 flushhalign.left = flushhalign(0)
425 flushhalign.center = flushhalign(0.5)
426 flushhalign.right = flushhalign(1)
427 # flushhalign.clear = attr.clearclass(flushhalign) # we can't defined a clearclass for flushhalign since it couldn't clear a halign's flushhalign
430 class halign(boxhalign, flushhalign, _localattr):
432 def __init__(self, aboxhalign, aflushhalign):
433 self.boxhalign = aboxhalign
434 self.flushhalign = aflushhalign
435 attr.exclusiveattr.__init__(self, halign)
437 def apply(self, expr):
438 return r"\gdef\PyXBoxHAlign{%.5f}\gdef\PyXFlushHAlign{%.5f}\PyXragged{}%s" % (self.boxhalign, self.flushhalign, expr)
440 halign.left = halign(0, 0)
441 halign.center = halign(0.5, 0.5)
442 halign.right = halign(1, 1)
443 halign.clear = attr.clearclass(halign)
444 halign.boxleft = boxhalign.left
445 halign.boxcenter = boxhalign.center
446 halign.boxright = boxhalign.right
447 halign.flushleft = halign.raggedright = flushhalign.left
448 halign.flushcenter = halign.raggedcenter = flushhalign.center
449 halign.flushright = halign.raggedleft = flushhalign.right
452 class _mathmode(attr.attr, textattr, _localattr):
453 "math mode"
455 def apply(self, expr):
456 return r"$\displaystyle{%s}$" % expr
458 mathmode = _mathmode()
459 clearmathmode = attr.clearclass(_mathmode)
462 class _phantom(attr.attr, textattr, _localattr):
463 "phantom text"
465 def apply(self, expr):
466 return r"\phantom{%s}" % expr
468 phantom = _phantom()
469 clearphantom = attr.clearclass(_phantom)
472 _textattrspreamble += "\\newbox\\PyXBoxVBox%\n\\newdimen\\PyXDimenVBox%\n"
474 class parbox_pt(attr.sortbeforeexclusiveattr, textattr):
476 top = 1
477 middle = 2
478 bottom = 3
480 def __init__(self, width, baseline=top):
481 self.width = width * 72.27 / (unit.scale["x"] * 72)
482 self.baseline = baseline
483 attr.sortbeforeexclusiveattr.__init__(self, parbox_pt, [_localattr])
485 def apply(self, expr):
486 if self.baseline == self.top:
487 return r"\linewidth=%.5ftruept\vtop{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
488 elif self.baseline == self.middle:
489 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)
490 elif self.baseline == self.bottom:
491 return r"\linewidth=%.5ftruept\vbox{\hsize=\linewidth\textwidth=\linewidth{}%s}" % (self.width, expr)
492 else:
493 ValueError("invalid baseline argument")
495 parbox_pt.clear = attr.clearclass(parbox_pt)
497 class parbox(parbox_pt):
499 def __init__(self, width, **kwargs):
500 parbox_pt.__init__(self, unit.topt(width), **kwargs)
502 parbox.clear = parbox_pt.clear
505 _textattrspreamble += "\\newbox\\PyXBoxVAlign%\n\\newdimen\\PyXDimenVAlign%\n"
507 class valign(attr.sortbeforeexclusiveattr, textattr):
509 def __init__(self, avalign):
510 self.valign = avalign
511 attr.sortbeforeexclusiveattr.__init__(self, valign, [parbox_pt, _localattr])
513 def apply(self, expr):
514 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)
516 valign.top = valign(0)
517 valign.middle = valign(0.5)
518 valign.bottom = valign(1)
519 valign.clear = valign.baseline = attr.clearclass(valign)
522 _textattrspreamble += "\\newdimen\\PyXDimenVShift%\n"
524 class _vshift(attr.sortbeforeattr, textattr):
526 def __init__(self):
527 attr.sortbeforeattr.__init__(self, [valign, parbox_pt, _localattr])
529 def apply(self, expr):
530 return r"%s\setbox0\hbox{{%s}}\lower\PyXDimenVShift\box0" % (self.setheightexpr(), expr)
532 class vshift(_vshift):
533 "vertical down shift by a fraction of a character height"
535 def __init__(self, lowerratio, heightstr="0"):
536 _vshift.__init__(self)
537 self.lowerratio = lowerratio
538 self.heightstr = heightstr
540 def setheightexpr(self):
541 return r"\setbox0\hbox{{%s}}\PyXDimenVShift=%.5f\ht0" % (self.heightstr, self.lowerratio)
543 class _vshiftmathaxis(_vshift):
544 "vertical down shift by the height of the math axis"
546 def setheightexpr(self):
547 return r"\setbox0\hbox{$\vcenter{\vrule width0pt}$}\PyXDimenVShift=\ht0"
550 vshift.bottomzero = vshift(0)
551 vshift.middlezero = vshift(0.5)
552 vshift.topzero = vshift(1)
553 vshift.mathaxis = _vshiftmathaxis()
554 vshift.clear = attr.clearclass(_vshift)
557 defaultsizelist = ["normalsize", "large", "Large", "LARGE", "huge", "Huge",
558 None, "tiny", "scriptsize", "footnotesize", "small"]
560 class size(attr.sortbeforeattr, textattr):
561 "font size"
563 def __init__(self, sizeindex=None, sizename=None, sizelist=defaultsizelist):
564 if (sizeindex is None and sizename is None) or (sizeindex is not None and sizename is not None):
565 raise ValueError("either specify sizeindex or sizename")
566 attr.sortbeforeattr.__init__(self, [_mathmode, _vshift])
567 if sizeindex is not None:
568 if sizeindex >= 0 and sizeindex < sizelist.index(None):
569 self.size = sizelist[sizeindex]
570 elif sizeindex < 0 and sizeindex + len(sizelist) > sizelist.index(None):
571 self.size = sizelist[sizeindex]
572 else:
573 raise IndexError("index out of sizelist range")
574 else:
575 self.size = sizename
577 def apply(self, expr):
578 return r"\%s{}%s" % (self.size, expr)
580 size.tiny = size(-4)
581 size.scriptsize = size.script = size(-3)
582 size.footnotesize = size.footnote = size(-2)
583 size.small = size(-1)
584 size.normalsize = size.normal = size(0)
585 size.large = size(1)
586 size.Large = size(2)
587 size.LARGE = size(3)
588 size.huge = size(4)
589 size.Huge = size(5)
590 size.clear = attr.clearclass(size)
593 ###############################################################################
594 # texrunner
595 ###############################################################################
598 class MonitorOutput(threading.Thread):
600 def __init__(self, name, output):
601 """Deadlock-safe output stream reader and monitor.
603 This method sets up a thread to continously read lines from a stream.
604 By that a deadlock due to a full pipe is prevented. In addition, the
605 stream content can be monitored for containing a certain string (see
606 :meth:`expect` and :meth:`wait`) and return all the collected output
607 (see :meth:`read`).
609 :param string name: name to be used while logging in :meth:`wait` and
610 :meth:`done`
611 :param file output: output stream
614 self.output = output
615 self._expect = queue.Queue(1)
616 self._received = threading.Event()
617 self._output = queue.Queue()
618 threading.Thread.__init__(self, name=name, daemon=1)
619 self.start()
621 def expect(self, s):
622 """Expect a string on a **single** line in the output.
624 This method must be called **before** the output occurs, i.e. before
625 the input is written to the TeX/LaTeX process.
627 :param s: expected string or ``None`` if output is expected to become
628 empty
629 :type s: str or None
632 self._expect.put_nowait(s)
634 def read(self):
635 """Read all output collected since its previous call.
637 The output reading should be synchronized by the :meth:`expect`
638 and :meth:`wait` methods.
640 :returns: collected output from the stream
641 :rtype: str
644 l = []
645 try:
646 while True:
647 l.append(self._output.get_nowait())
648 except queue.Empty:
649 pass
650 return "".join(l).replace("\r\n", "\n").replace("\r", "\n")
652 def _wait(self, waiter, checker):
653 """Helper method to implement :meth:`wait` and :meth:`done`.
655 Waits for an event using the *waiter* and *checker* functions while
656 providing user feedback to the ``pyx``-logger using the warning level
657 according to the ``wait`` and ``showwait`` from the ``text`` section of
658 the pyx :mod:`config`.
660 :param function waiter: callback to wait for (the function gets called
661 with a timeout parameter)
662 :param function checker: callback returing ``True`` if
663 waiting was successful
664 :returns: ``True`` when wait was successful
665 :rtype: bool
668 wait = config.getint("text", "wait", 60)
669 showwait = config.getint("text", "showwait", 5)
670 if showwait:
671 waited = 0
672 hasevent = False
673 while waited < wait and not hasevent:
674 if wait - waited > showwait:
675 waiter(showwait)
676 waited += showwait
677 else:
678 waiter(wait - waited)
679 waited += wait - waited
680 hasevent = checker()
681 if not hasevent:
682 if waited < wait:
683 logger.warning("Still waiting for {} "
684 "after {} (of {}) seconds..."
685 .format(self.name, waited, wait))
686 else:
687 logger.warning("The timeout of {} seconds expired "
688 "and {} did not respond."
689 .format(waited, self.name))
690 return hasevent
691 else:
692 waiter(wait)
693 return checker()
695 def wait(self):
696 """Wait for the expected output to happen.
698 Waits either until a line containing the string set by the previous
699 :meth:`expect` call is found, or a timeout occurs.
701 :returns: ``True`` when the expected string was found
702 :rtype: bool
705 r = self._wait(self._received.wait, self._received.isSet)
706 if r:
707 self._received.clear()
708 return r
710 def done(self):
711 """Waits until the output becomes empty.
713 Waits either until the output becomes empty, or a timeout occurs.
714 The generated output can still be catched by :meth:`read` after
715 :meth:`done` was successful.
717 In the proper workflow :meth:`expect` should be called with ``None``
718 before the output completes, as otherwise a ``ValueError`` is raised
719 in the :meth:`run`.
721 :returns: ``True`` when the output has become empty
722 :rtype: bool
725 return self._wait(self.join, lambda self=self: not self.is_alive())
727 def _readline(self):
728 """Read a line from the output.
730 To be used **inside** the thread routine only.
732 :returns: one line of the output as a string
733 :rtype: str
736 while True:
737 try:
738 return self.output.readline()
739 except IOError as e:
740 if e.errno != errno.EINTR:
741 raise
743 def run(self):
744 """Thread routine.
746 **Not** to be called from outside.
748 :raises ValueError: output becomes empty while some string is expected
751 expect = None
752 while True:
753 line = self._readline()
754 if expect is None:
755 try:
756 expect = self._expect.get_nowait()
757 except queue.Empty:
758 pass
759 if not line:
760 break
761 self._output.put(line)
762 if expect is not None:
763 found = line.find(expect)
764 if found != -1:
765 self._received.set()
766 expect = None
767 self.output.close()
768 if expect is not None:
769 raise ValueError("{} finished unexpectedly".format(self.name))
772 class textbox(box.rect, canvas.canvas):
773 """basically a box.rect, but it contains a text created by the texrunner
774 - texrunner._text and texrunner.text return such an object
775 - _textbox instances can be inserted into a canvas
776 - the output is contained in a page of the dvifile available thru the texrunner"""
777 # TODO: shouldn't all boxes become canvases? how about inserts then?
779 def __init__(self, x, y, left, right, height, depth, do_finish, attrs):
781 - do_finish is a method to be called to get the dvicanvas
782 (e.g. the do_finish calls the setdvicanvas method)
783 - attrs are fillstyles"""
784 self.left = left
785 self.right = right
786 self.width = left + right
787 self.height = height
788 self.depth = depth
789 self.texttrafo = trafo.scale(unit.scale["x"]).translated(x, y)
790 box.rect.__init__(self, x - left, y - depth, left + right, depth + height, abscenter = (left, depth))
791 canvas.canvas.__init__(self, attrs)
792 self.do_finish = do_finish
793 self.dvicanvas = None
794 self.insertdvicanvas = False
796 def transform(self, *trafos):
797 if self.insertdvicanvas:
798 raise ValueError("can't apply transformation after dvicanvas was inserted")
799 box.rect.transform(self, *trafos)
800 for trafo in trafos:
801 self.texttrafo = trafo * self.texttrafo
803 def setdvicanvas(self, dvicanvas):
804 if self.dvicanvas is not None:
805 raise ValueError("multiple call to setdvicanvas")
806 self.dvicanvas = dvicanvas
808 def ensuredvicanvas(self):
809 if self.dvicanvas is None:
810 self.do_finish()
811 assert self.dvicanvas is not None, "do_finish is broken"
812 if not self.insertdvicanvas:
813 self.insert(self.dvicanvas, [self.texttrafo])
814 self.insertdvicanvas = True
816 def marker(self, marker):
817 self.ensuredvicanvas()
818 return self.texttrafo.apply(*self.dvicanvas.markers[marker])
820 def textpath(self):
821 self.ensuredvicanvas()
822 textpath = path.path()
823 for item in self.dvicanvas.items:
824 try:
825 textpath += item.textpath()
826 except AttributeError:
827 # ignore color settings etc.
828 pass
829 return textpath.transformed(self.texttrafo)
831 def processPS(self, file, writer, context, registry, bbox):
832 self.ensuredvicanvas()
833 abbox = bboxmodule.empty()
834 canvas.canvas.processPS(self, file, writer, context, registry, abbox)
835 bbox += box.rect.bbox(self)
837 def processPDF(self, file, writer, context, registry, bbox):
838 self.ensuredvicanvas()
839 abbox = bboxmodule.empty()
840 canvas.canvas.processPDF(self, file, writer, context, registry, abbox)
841 bbox += box.rect.bbox(self)
844 class _marker:
845 pass
848 class Tee(object):
850 def __init__(self, *files):
851 self.files = files
853 def write(self, data):
854 for file in self.files:
855 file.write(data)
857 def flush(self):
858 for file in self.files:
859 file.flush()
861 def close(self):
862 for file in self.files:
863 file.close()
865 # The texrunner state represents the next (or current) execute state.
866 STATE_START, STATE_PREAMBLE, STATE_TYPESET, STATE_DONE = range(4)
867 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:")
869 class _texrunner:
871 defaulttexmessagesstart = [texmessage.start]
872 defaulttexmessagesend = [texmessage.end, texmessage.font_warning, texmessage.rerun_warning, texmessage.nobbl_warning]
873 defaulttexmessagesdefaultpreamble = [texmessage.load]
874 defaulttexmessagesdefaultrun = [texmessage.font_warning, texmessage.box_warning, texmessage.package_warning,
875 texmessage.load_def, texmessage.load_graphics]
877 def __init__(self, executable,
878 texenc="ascii",
879 usefiles=[],
880 texipc=config.getboolean("text", "texipc", 0),
881 texdebug=None,
882 dvidebug=False,
883 errordebug=1,
884 texmsgstart=[],
885 texmsgend=[],
886 texmsgpreamble=[],
887 texmsgrun=[]):
888 """Base class for the TeX interface.
890 .. note:: This class cannot be used directly. It is the base class for
891 all texrunners and provides most of the implementation.
892 Still, to the end user the parameters except for *executable*
893 are important, as they are preserved in derived classes
894 usually.
896 :param str executable: command to start the TeX interpreter
897 :param str texenc: encoding to use in the communication with the TeX
898 interpreter
899 :param usefiles: list of supplementary files
900 :type usefiles: list of str
901 :param bool texipc: :ref:`texipc` flag. The default value shown is an
902 artifact of the documentation build environment. The actual default
903 is defined by the *texipc* option of the *text* section of the
904 :ref:`config`.
905 :param texdebug: filename or file to be used to store a copy of all the
906 input passed to the TeX interpreter
907 :type texdebug: None or str or file
908 :param bool dvidebug: ``True`` to turn on dvitype-like output
909 :param int errordebug: verbosity of the TexResultError messages
910 (``0`` means without the input and output details,
911 ``1`` means input and parsed output shortend to 5 lines, and
912 ``2`` means full input and unparsed as well as parsed output)
913 :param texmsgstart: additional message parsers to parse the TeX
914 interpreter startup
915 :type texmsgstart: list of texmsg
916 :param texmsgend: additional message parsers to parse the TeX
917 interpreter shutdown
918 :type texmsgend: list of texmsg
919 :param texmsgpreamble: additional message parsers to parse the preamble
920 output
921 :type texmsgpreamble: list of texmsg
922 :param texmsgrun: additional message parsers to parse the typeset
923 output
924 :type texmsgrun: list of texmsg
926 self.executable = executable
927 self.texenc = texenc
928 self.usefiles = usefiles
929 self.texipc = texipc
930 self.texdebug = texdebug
931 self.dvidebug = dvidebug
932 self.errordebug = errordebug
933 self.texmsgstart = texmsgstart
934 self.texmsgend = texmsgend
935 self.texmsgpreamble = texmsgpreamble
936 self.texmsgrun = texmsgrun
938 self.state = STATE_START
939 self.executeid = 0
940 self.page = 0
941 self.preambles = []
943 self.needdvitextboxes = [] # when texipc-mode off
944 self.dvifile = None
945 self.textboxesincluded = 0
947 self.tmpdir = None
949 def _cleanup(self):
950 """Clean-up TeX interpreter and tmp directory.
952 This funtion is hooked up in atexit to quit the TeX interpreter, to
953 save contents of the usefile files, and to remove the temporary
954 directory.
957 try:
958 if self.state > STATE_START:
959 if self.state < STATE_DONE:
960 self.do_finish()
961 if self.state < STATE_DONE: # cleanup while TeX is still running?
962 self.texoutput.expect(None)
963 self.force_done()
964 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)),
965 (self.popen.terminate, "Failed, too. Trying by kill signal now ..."),
966 (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))]:
968 if self.texoutput.done():
969 break
970 logger.warning(msg)
971 for usefile in self.usefiles:
972 extpos = usefile.rfind(".")
973 try:
974 os.rename(os.path.join(self.tmpdir, "texput" + usefile[extpos:]), usefile)
975 except EnvironmentError:
976 logger.warning("Could not save '{}'.".format(usefile))
977 if os.path.isfile(usefile):
978 try:
979 os.unlink(usefile)
980 except EnvironmentError:
981 logger.warning("Failed to remove spurious file '{}'.".format(usefile))
982 finally:
983 shutil.rmtree(self.tmpdir, ignore_errors=True)
985 def _execute(self, expr, texmessages, oldstate, newstate):
986 """executes expr within TeX/LaTeX"""
987 assert STATE_PREAMBLE <= oldstate <= STATE_TYPESET
988 assert oldstate == self.state
989 assert newstate >= oldstate
990 if newstate == STATE_DONE:
991 self.texoutput.expect(None)
992 self.texinput.write(expr)
993 else:
994 if oldstate == newstate == STATE_TYPESET:
995 self.page += 1
996 expr = "\\ProcessPyXBox{%s%%\n}{%i}" % (expr, self.page)
997 self.executeid += 1
998 self.texoutput.expect("PyXInputMarker:executeid=%i:" % self.executeid)
999 expr += "%%\n\\PyXInput{%i}%%\n" % self.executeid
1000 self.texinput.write(expr)
1001 self.texinput.flush()
1002 self.state = newstate
1003 if newstate == STATE_DONE:
1004 wait_ok = self.texoutput.done()
1005 else:
1006 wait_ok = self.texoutput.wait()
1007 try:
1008 parsed = unparsed = self.texoutput.read()
1009 if not wait_ok:
1010 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1011 if newstate != STATE_DONE:
1012 parsed, m = remove_string("PyXInputMarker:executeid=%s:" % self.executeid, parsed)
1013 if not m:
1014 raise TexResultError("PyXInputMarker expected")
1015 if oldstate == newstate == STATE_TYPESET:
1016 parsed, m = remove_pattern(PyXBoxPattern, parsed, ignore_nl=False)
1017 if not m:
1018 raise TexResultError("PyXBox expected")
1019 if m.group("page") != str(self.page):
1020 raise TexResultError("Wrong page number in PyXBox")
1021 extent = [float(x)*72/72.27*unit.x_pt for x in m.group("lt", "rt", "ht", "dp")]
1022 parsed, m = remove_string("[80.121.88.%s]" % self.page, parsed)
1023 if not m:
1024 raise TexResultError("PyXPageOutMarker expected")
1025 for t in texmessages:
1026 parsed = t(parsed, self.page)
1027 if parsed.replace(r"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1028 raise TexResultError("unhandled TeX response (might be an error)")
1029 except TexResultError as e:
1030 if self.errordebug > 0:
1031 def add(msg): e.args = (e.args[0] + msg,)
1032 add("\nThe expression passed to TeX was:\n{}".format(textwrap.indent(expr.rstrip(), " ")))
1033 if self.errordebug >= 2:
1034 add("\nThe return message from TeX was:\n{}".format(textwrap.indent(unparsed.rstrip(), " ")))
1035 if self.errordebug == 1:
1036 if parsed.count('\n') > 5:
1037 parsed = "\n".join(parsed.split("\n")[:5] + ["(cut after 5 lines; use errordebug=2 for all output)"])
1038 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(textwrap.indent(parsed.rstrip(), " ")))
1039 raise e
1040 if oldstate == newstate == STATE_TYPESET:
1041 return extent
1043 def do_start(self):
1044 assert self.state == STATE_START
1045 self.state = STATE_PREAMBLE
1047 if self.tmpdir is None:
1048 self.tmpdir = tempfile.mkdtemp()
1049 atexit.register(self._cleanup)
1050 for usefile in self.usefiles:
1051 extpos = usefile.rfind(".")
1052 try:
1053 os.rename(usefile, os.path.join(self.tmpdir, "texput" + usefile[extpos:]))
1054 except OSError:
1055 pass
1056 cmd = [self.executable, '--output-directory', self.tmpdir]
1057 if self.texipc:
1058 cmd.append("--ipc")
1059 self.popen = config.Popen(cmd, stdin=config.PIPE, stdout=config.PIPE, stderr=config.STDOUT, bufsize=0)
1060 self.texinput = io.TextIOWrapper(self.popen.stdin, encoding=self.texenc)
1061 if self.texdebug:
1062 try:
1063 self.texdebug.write
1064 except AttributeError:
1065 self.texinput = Tee(open(self.texdebug, "w", encoding=self.texenc), self.texinput)
1066 else:
1067 self.texinput = Tee(self.texdebug, self.texinput)
1068 self.texoutput = MonitorOutput(self.name, io.TextIOWrapper(self.popen.stdout, encoding=self.texenc))
1069 self._execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1070 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1071 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1072 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1073 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1074 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1075 "\\newdimen\\PyXDimenHAlignRT%\n" +
1076 _textattrspreamble + # insert preambles for textattrs macros
1077 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1078 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1079 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1080 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1081 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1082 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1083 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1084 "lt=\\the\\PyXDimenHAlignLT,"
1085 "rt=\\the\\PyXDimenHAlignRT,"
1086 "ht=\\the\\ht\\PyXBox,"
1087 "dp=\\the\\dp\\PyXBox:}%\n"
1088 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1089 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1090 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1091 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1092 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1093 self.defaulttexmessagesstart + self.texmsgstart, STATE_PREAMBLE, STATE_PREAMBLE)
1095 def do_preamble(self, expr, texmessages):
1096 if self.state < STATE_PREAMBLE:
1097 self.do_start()
1098 self._execute(expr, texmessages, STATE_PREAMBLE, STATE_PREAMBLE)
1100 def do_typeset(self, expr, texmessages):
1101 if self.state < STATE_PREAMBLE:
1102 self.do_start()
1103 if self.state < STATE_TYPESET:
1104 self.go_typeset()
1105 return self._execute(expr, texmessages, STATE_TYPESET, STATE_TYPESET)
1107 def do_finish(self):
1108 if self.state < STATE_TYPESET:
1109 self.go_typeset()
1110 self.go_finish()
1111 self.texinput.close() # close the input queue and
1112 self.texoutput.done() # wait for finish of the output
1114 if not self.texipc:
1115 dvifilename = os.path.join(self.tmpdir, "texput.dvi")
1116 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvidebug)
1117 page = 1
1118 for box in self.needdvitextboxes:
1119 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0], fontmap=box.fontmap, singlecharmode=box.singlecharmode))
1120 page += 1
1121 if self.dvifile.readpage(None) is not None:
1122 raise ValueError("end of dvifile expected but further pages follow")
1124 self.dvifile = None
1125 self.needdvitextboxes = []
1127 def reset(self, reinit=0):
1128 "resets the tex runner to its initial state (upto its record to old dvi file(s))"
1129 assert self.state > STATE_START
1130 if self.state < STATE_DONE:
1131 self.do_finish()
1132 self.executeid = 0
1133 self.page = 0
1134 self.state = STATE_START
1135 if reinit:
1136 for expr, texmessages in self.preambles:
1137 self.do_preamble(expr, texmessages)
1138 else:
1139 self.preambles = []
1141 def preamble(self, expr, texmessages=[]):
1142 r"""put something into the TeX/LaTeX preamble
1143 - in LaTeX, this is done before the \begin{document}
1144 (you might use \AtBeginDocument, when you're in need for)
1145 - it is not allowed to call preamble after calling the
1146 text method for the first time (for LaTeX this is needed
1147 due to \begin{document}; in TeX it is forced for compatibility
1148 (you should be able to switch from TeX to LaTeX, if you want,
1149 without breaking something)
1150 - preamble expressions must not create any dvi output
1151 - args might contain texmessage instances"""
1152 texmessages = self.defaulttexmessagesdefaultpreamble + self.texmsgpreamble + texmessages
1153 self.preambles.append((expr, texmessages))
1154 self.do_preamble(expr, texmessages)
1156 def text(self, x, y, expr, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1157 """create text by passing expr to TeX/LaTeX
1158 - returns a textbox containing the result from running expr thru TeX/LaTeX
1159 - the box center is set to x, y
1160 - *args may contain attr parameters, namely:
1161 - textattr instances
1162 - texmessage instances
1163 - trafo._trafo instances
1164 - style.fillstyle instances"""
1165 if expr is None:
1166 raise ValueError("None expression is invalid")
1167 if self.state == STATE_DONE:
1168 self.reset(reinit=1)
1169 textattrs = attr.mergeattrs(textattrs) # perform cleans
1170 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1171 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1172 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1173 textattrs = attr.getattrs(textattrs, [textattr])
1174 for ta in textattrs[::-1]:
1175 expr = ta.apply(expr)
1176 first = self.state < STATE_TYPESET
1177 left, right, height, depth = self.do_typeset(expr, self.defaulttexmessagesdefaultrun + self.texmsgrun + texmessages)
1178 if self.texipc and first:
1179 self.dvifile = dvifile.DVIfile(os.path.join(self.tmpdir, "texput.dvi"), debug=self.dvidebug)
1180 box = textbox(x, y, left, right, height, depth, self.do_finish, fillstyles)
1181 for t in trafos:
1182 box.reltransform(t) # TODO: should trafos really use reltransform???
1183 # this is quite different from what we do elsewhere!!!
1184 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1185 if self.texipc:
1186 box.setdvicanvas(self.dvifile.readpage([ord("P"), ord("y"), ord("X"), self.page, 0, 0, 0, 0, 0, 0], fontmap=fontmap, singlecharmode=singlecharmode))
1187 else:
1188 box.fontmap = fontmap
1189 box.singlecharmode = singlecharmode
1190 self.needdvitextboxes.append(box)
1191 return box
1193 def text_pt(self, x, y, expr, *args, **kwargs):
1194 return self.text(x * unit.t_pt, y * unit.t_pt, expr, *args, **kwargs)
1197 class texrunner(_texrunner):
1199 def __init__(self, executable=config.get("text", "tex", "tex"), lfs="10pt", **kwargs):
1200 super().__init__(executable=executable, **kwargs)
1201 self.lfs = lfs
1202 self.name = "TeX"
1204 def go_typeset(self):
1205 assert self.state == STATE_PREAMBLE
1206 self.state = STATE_TYPESET
1208 def go_finish(self):
1209 self._execute("\\end%\n", self.defaulttexmessagesend + self.texmsgend, STATE_TYPESET, STATE_DONE)
1211 def force_done(self):
1212 self.texinput.write("\n\\end\n")
1214 def do_start(self):
1215 super().do_start()
1216 if self.lfs:
1217 if not self.lfs.endswith(".lfs"):
1218 self.lfs = "%s.lfs" % self.lfs
1219 with config.open(self.lfs, []) as lfsfile:
1220 lfsdef = lfsfile.read().decode("ascii")
1221 self._execute(lfsdef, [], STATE_PREAMBLE, STATE_PREAMBLE)
1222 self._execute("\\normalsize%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1223 self._execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1226 class latexrunner(_texrunner):
1228 defaulttexmessagesdocclass = [texmessage.load]
1229 defaulttexmessagesbegindoc = [texmessage.load, texmessage.no_aux]
1231 def __init__(self, executable=config.get("text", "latex", "latex"),
1232 docclass="article", docopt=None, pyxgraphics=True,
1233 texmessagesdocclass=[], texmessagesbegindoc=[], **kwargs):
1234 super().__init__(executable=executable, **kwargs)
1235 self.docclass = docclass
1236 self.docopt = docopt
1237 self.pyxgraphics = pyxgraphics
1238 self.texmessagesdocclass = texmessagesdocclass
1239 self.texmessagesbegindoc = texmessagesbegindoc
1240 self.name = "LaTeX"
1242 def go_typeset(self):
1243 self._execute("\\begin{document}", self.defaulttexmessagesbegindoc + self.texmessagesbegindoc, STATE_PREAMBLE, STATE_TYPESET)
1245 def go_finish(self):
1246 self._execute("\\end{document}%\n", self.defaulttexmessagesend + self.texmsgend, STATE_TYPESET, STATE_DONE)
1248 def force_done(self):
1249 self.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1251 def do_start(self):
1252 super().do_start()
1253 if self.pyxgraphics:
1254 with config.open("pyx.def", []) as source, open(os.path.join(self.tmpdir, "pyx.def"), "wb") as dest:
1255 dest.write(source.read())
1256 self._execute("\\makeatletter%\n"
1257 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1258 "\\def\\ProcessOptions{%\n"
1259 "\\def\\Gin@driver{" + self.tmpdir.replace(os.sep, "/") + "/pyx.def}%\n"
1260 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1261 "\\saveProcessOptions}%\n"
1262 "\\makeatother",
1263 [], STATE_PREAMBLE, STATE_PREAMBLE)
1264 if self.docopt is not None:
1265 self._execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
1266 self.defaulttexmessagesdocclass + self.texmessagesdocclass, STATE_PREAMBLE, STATE_PREAMBLE)
1267 else:
1268 self._execute("\\documentclass{%s}" % self.docclass,
1269 self.defaulttexmessagesdocclass + self.texmessagesdocclass, STATE_PREAMBLE, STATE_PREAMBLE)
1272 def set(mode="tex", **kwargs):
1273 global defaulttexrunner, reset, preamble, text, text_pt
1274 mode = mode.lower()
1275 if mode == "tex":
1276 defaulttexrunner = texrunner(**kwargs)
1277 elif mode == "latex":
1278 defaulttexrunner = latexrunner(**kwargs)
1279 else:
1280 raise ValueError("mode \"TeX\" or \"LaTeX\" expected")
1281 reset = defaulttexrunner.reset
1282 preamble = defaulttexrunner.preamble
1283 text = defaulttexrunner.text
1284 text_pt = defaulttexrunner.text_pt
1286 set()
1288 def escapestring(s, replace={" ": "~",
1289 "$": "\\$",
1290 "&": "\\&",
1291 "#": "\\#",
1292 "_": "\\_",
1293 "%": "\\%",
1294 "^": "\\string^",
1295 "~": "\\string~",
1296 "<": "{$<$}",
1297 ">": "{$>$}",
1298 "{": "{$\{$}",
1299 "}": "{$\}$}",
1300 "\\": "{$\setminus$}",
1301 "|": "{$\mid$}"}):
1302 "escape all ascii characters such that they are printable by TeX/LaTeX"
1303 i = 0
1304 while i < len(s):
1305 if not 32 <= ord(s[i]) < 127:
1306 raise ValueError("escapestring function handles ascii strings only")
1307 c = s[i]
1308 try:
1309 r = replace[c]
1310 except KeyError:
1311 i += 1
1312 else:
1313 s = s[:i] + r + s[i+1:]
1314 i += len(r)
1315 return s