fix cross-device link error
[PyX.git] / pyx / text.py
blobc2dd33b448e8b5eb17cc01fef673f89d80bce490
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 ... ''')
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''')
278 'other things'
279 >>> texmessage.load(r'''other ("text.py") things''')
280 'other things'
281 >>> texmessage.load(r'''other ("tex
282 ... t.py" further (ignored)
283 ... text) things''')
284 'other things'
285 >>> texmessage.load(r'''other (t
286 ... ext
287 ... .py
288 ... fur
289 ... ther (ignored) text) things''')
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(config.get("text", "chroot", "") + 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(config.get("text", "chroot", "") + 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(config.get("text", "chroot", "") + m.group("filename")):
320 return msg
321 r, m = remove_pattern(p, 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(config.get("text", "chroot", "") + 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 An instance of this class creates a thread to continously read lines
618 from a stream. By that a deadlock due to a full pipe is prevented. In
619 addition, the stream content can be monitored for containing a certain
620 string (see :meth:`expect` and :meth:`wait`) and return all the
621 collected output (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_pt(box.rect, baseclasses.canvasitem): pass
790 class textextbox_pt(textbox_pt):
792 def __init__(self, x_pt, y_pt, left_pt, right_pt, height_pt, depth_pt, do_finish, fontmap, singlecharmode, fillstyles):
793 """Text output.
795 An instance of this class contains the text output generated by PyX. It
796 is a :class:`baseclasses.canvasitem` and thus can be inserted into a
797 canvas.
799 .. A text has a center (x_pt, y_pt) as well as extents in x-direction
800 .. (left_pt and right_pt) and y-direction (hight_pt and depth_pt). The
801 .. textbox positions the output accordingly and scales it by the
802 .. x-scale from the :mod:`unit`.
804 .. :param float x_pt: x coordinate of the center in pts
805 .. :param float y_pt: y coordinate of the center in pts
806 .. :param float left_pt: unscaled left extent in pts
807 .. :param float right_pt: unscaled right extent in pts
808 .. :param float height_pt: unscaled height in pts
809 .. :param float depth_pt: unscaled depth in pts
810 .. :param function do_finish: callable to execute :meth:`readdvipage`
811 .. :param fontmap: force a fontmap to be used (instead of the default
812 .. depending on the output format)
813 .. :type fontmap: None or fontmap
814 .. :param bool singlecharmode: position each character separately
815 .. :param fillstyles: fill styles to be applied
816 .. :type fillstyles: list of fillstyles
819 self.left = left_pt*unit.x_pt #: left extent of the text (PyX length)
820 self.right = right_pt*unit.x_pt #: right extent of the text (PyX length)
821 self.width = self.left + self.right #: width of the text (PyX length)
822 self.height = height_pt*unit.x_pt #: height of the text (PyX length)
823 self.depth = depth_pt*unit.x_pt #: height of the text (PyX length)
825 self.do_finish = do_finish
826 self.fontmap = fontmap
827 self.singlecharmode = singlecharmode
828 self.fillstyles = fillstyles
830 self.texttrafo = trafo.scale(unit.scale["x"]).translated_pt(x_pt, y_pt)
831 # box.rect_pt.__init__(self, x_pt - left_pt*unit.scale["x"], y_pt - depth_pt*unit.scale["x"],
832 # (left_pt + right_pt)*unit.scale["x"],
833 # (depth_pt + height_pt)*unit.scale["x"],
834 # abscenter_pt = (left_pt*unit.scale["x"], depth_pt*unit.scale["x"]))
835 box.rect.__init__(self, -self.left, -self.depth, self.left+self.right, self.depth+self.height, abscenter = (self.left, self.depth))
836 box.rect.transform(self, self.texttrafo)
838 self._dvicanvas = None
840 def transform(self, *trafos):
841 box.rect.transform(self, *trafos)
842 for trafo in trafos:
843 self.texttrafo = trafo * self.texttrafo
844 if self._dvicanvas is not None:
845 for trafo in trafos:
846 self._dvicanvas.trafo = trafo * self._dvicanvas.trafo
848 def readdvipage(self, dvifile, page):
849 self._dvicanvas = dvifile.readpage([ord("P"), ord("y"), ord("X"), page, 0, 0, 0, 0, 0, 0],
850 fontmap=self.fontmap, singlecharmode=self.singlecharmode, attrs=[self.texttrafo] + self.fillstyles)
852 @property
853 def dvicanvas(self):
854 if self._dvicanvas is None:
855 self.do_finish()
856 return self._dvicanvas
858 def marker(self, name):
859 """Return the position of a marker.
861 :param str name: name of the marker
862 :returns: position of the marker
863 :rtype: tuple of two PyX lengths
865 This method returns the position of the marker of the given name
866 within, previously defined by the ``\\PyXMarker{name}`` macro in the
867 typeset text. Please do not use the ``@`` character within your name to
868 prevent name clashes with PyX internal uses (although we don’t the
869 marker feature internally right now).
871 Similar to generating actual output, the marker function accesses the
872 DVI output, stopping. The :ref:`texipc` feature will allow for this access
873 without stopping the TeX interpreter.
876 return self.texttrafo.apply(*self.dvicanvas.markers[name])
878 def textpath(self):
879 textpath = path.path()
880 for item in self.dvicanvas.items:
881 textpath += item.textpath()
882 return textpath.transformed(self.texttrafo)
884 def processPS(self, file, writer, context, registry, bbox):
885 abbox = bboxmodule.empty()
886 self.dvicanvas.processPS(file, writer, context, registry, abbox)
887 bbox += box.rect.bbox(self)
889 def processPDF(self, file, writer, context, registry, bbox):
890 abbox = bboxmodule.empty()
891 self.dvicanvas.processPDF(file, writer, context, registry, abbox)
892 bbox += box.rect.bbox(self)
894 def processSVG(self, xml, writer, context, registry, bbox):
895 abbox = bboxmodule.empty()
896 self.dvicanvas.processSVG(xml, writer, context, registry, abbox)
897 bbox += box.rect.bbox(self)
900 class _marker:
901 pass
904 class errordetail:
905 "Constants defining the verbosity of the :exc:`TexResultError`."
906 none = 0 #: Without any input and output.
907 default = 1 #: Input and parsed output shortend to 5 lines.
908 full = 2 #: Full input and unparsed as well as parsed output.
911 class Tee(object):
913 def __init__(self, *files):
914 """Apply write, flush, and close to each of the given files."""
915 self.files = files
917 def write(self, data):
918 for file in self.files:
919 file.write(data)
921 def flush(self):
922 for file in self.files:
923 file.flush()
925 def close(self):
926 for file in self.files:
927 file.close()
929 # The texrunner state represents the next (or current) execute state.
930 STATE_START, STATE_PREAMBLE, STATE_TYPESET, STATE_DONE = range(4)
931 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:")
932 dvi_pattern = re.compile(r"Output written on .*texput\.dvi \((?P<page>\d+) pages?, \d+ bytes\)\.", re.DOTALL)
934 class TexDoneError(Exception):
935 pass
938 class SingleRunner:
940 #: default :class:`texmessage` parsers at interpreter startup
941 texmessages_start_default = [texmessage.start]
942 #: default :class:`texmessage` parsers at interpreter shutdown
943 texmessages_end_default = [texmessage.end, texmessage.font_warning, texmessage.rerun_warning, texmessage.nobbl_warning]
944 #: default :class:`texmessage` parsers for preamble output
945 texmessages_preamble_default = [texmessage.load]
946 #: default :class:`texmessage` parsers for typeset output
947 texmessages_run_default = [texmessage.font_warning, texmessage.box_warning, texmessage.package_warning,
948 texmessage.load_def, texmessage.load_graphics]
950 def __init__(self, cmd,
951 texenc="ascii",
952 usefiles=[],
953 texipc=config.getboolean("text", "texipc", 0),
954 copyinput=None,
955 dvitype=False,
956 errordetail=errordetail.default,
957 texmessages_start=[],
958 texmessages_end=[],
959 texmessages_preamble=[],
960 texmessages_run=[]):
961 """Base class for the TeX interface.
963 .. note:: This class cannot be used directly. It is the base class for
964 all texrunners and provides most of the implementation.
965 Still, to the end user the parameters except for *cmd*
966 are important, as they are preserved in derived classes
967 usually.
969 :param cmd: command and arguments to start the TeX interpreter
970 :type cmd: list of str
971 :param str texenc: encoding to use in the communication with the TeX
972 interpreter
973 :param usefiles: list of supplementary files to be copied to and from
974 the temporary working directory (see :ref:`debug` for usage
975 details)
976 :type usefiles: list of str
977 :param bool texipc: :ref:`texipc` flag.
978 :param copyinput: filename or file to be used to store a copy of all
979 the input passed to the TeX interpreter
980 :type copyinput: None or str or file
981 :param bool dvitype: flag to turn on dvitype-like output
982 :param errordetail: verbosity of the :exc:`TexResultError`
983 :type errordetail: :class:`errordetail`
984 :param texmessages_start: additional message parsers at interpreter
985 startup
986 :type texmessages_start: list of :class:`texmessage` parsers
987 :param texmessages_end: additional message parsers at interpreter
988 shutdown
989 :type texmessages_end: list of :class:`texmessage` parsers
990 :param texmessages_preamble: additional message parsers for preamble
991 output
992 :type texmessages_preamble: list of :class:`texmessage` parsers
993 :param texmessages_run: additional message parsers for typset output
994 :type texmessages_run: list of :class:`texmessage` parsers
997 self.cmd = cmd
998 self.texenc = texenc
999 self.usefiles = usefiles
1000 self.texipc = texipc
1001 self.copyinput = copyinput
1002 self.dvitype = dvitype
1003 self.errordetail = errordetail
1004 self.texmessages_start = texmessages_start
1005 self.texmessages_end = texmessages_end
1006 self.texmessages_preamble = texmessages_preamble
1007 self.texmessages_run = texmessages_run
1009 self.state = STATE_START
1010 self.executeid = 0
1011 self.page = 0
1013 self.needdvitextboxes = [] # when texipc-mode off
1014 self.dvifile = None
1016 def _cleanup(self):
1017 """Clean-up TeX interpreter and tmp directory.
1019 This funtion is hooked up in atexit to quit the TeX interpreter, to
1020 save the contents of usefiles, and to remove the temporary directory.
1023 try:
1024 if self.state > STATE_START:
1025 if self.state < STATE_DONE:
1026 self.do_finish()
1027 if self.state < STATE_DONE: # cleanup while TeX is still running?
1028 self.texoutput.expect(None)
1029 self.force_done()
1030 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)),
1031 (self.popen.terminate, "Failed, too. Trying by kill signal now ..."),
1032 (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))]:
1034 if self.texoutput.done():
1035 break
1036 logger.warning(msg)
1037 for usefile in self.usefiles:
1038 extpos = usefile.rfind(".")
1039 try:
1040 shutil.move(os.path.join(self.tmpdir, "texput" + usefile[extpos:]), usefile)
1041 except EnvironmentError:
1042 logger.warning("Could not save '{}'.".format(usefile))
1043 if os.path.isfile(usefile):
1044 try:
1045 os.unlink(usefile)
1046 except EnvironmentError:
1047 logger.warning("Failed to remove spurious file '{}'.".format(usefile))
1048 finally:
1049 shutil.rmtree(self.tmpdir, ignore_errors=True)
1051 def _execute(self, expr, texmessages, oldstate, newstate):
1052 """Execute TeX expression.
1054 :param str expr: expression to be passed to TeX
1055 :param texmessages: message parsers to analyse the textual output of
1057 :type texmessages: list of :class:`texmessage` parsers
1058 :param int oldstate: state of the TeX interpreter prior to the
1059 expression execution
1060 :param int newstate: state of the TeX interpreter after to the
1061 expression execution
1064 assert STATE_PREAMBLE <= oldstate <= STATE_TYPESET
1065 assert oldstate == self.state
1066 assert newstate >= oldstate
1067 if newstate == STATE_DONE:
1068 self.texoutput.expect(None)
1069 self.texinput.write(expr)
1070 else:
1072 # test to encode expr early to not pile up expected results
1073 # if the expression won't make it to the texinput at all
1074 # (which would otherwise harm a proper cleanup)
1075 expr.encode(self.texenc)
1077 if oldstate == newstate == STATE_TYPESET:
1078 self.page += 1
1079 expr = "\\ProcessPyXBox{%s%%\n}{%i}" % (expr, self.page)
1080 self.executeid += 1
1081 self.texoutput.expect("PyXInputMarker:executeid=%i:" % self.executeid)
1082 expr += "%%\n\\PyXInput{%i}%%\n" % self.executeid
1083 self.texinput.write(expr)
1084 self.texinput.flush()
1085 self.state = newstate
1086 if newstate == STATE_DONE:
1087 wait_ok = self.texoutput.done()
1088 else:
1089 wait_ok = self.texoutput.wait()
1090 try:
1091 parsed = unparsed = self.texoutput.read()
1092 if not wait_ok:
1093 raise TexResultError("TeX didn't respond as expected within the timeout period.")
1094 if newstate != STATE_DONE:
1095 parsed, m = remove_string("PyXInputMarker:executeid=%s:" % self.executeid, parsed)
1096 if not m:
1097 raise TexResultError("PyXInputMarker expected")
1098 if oldstate == newstate == STATE_TYPESET:
1099 parsed, m = remove_pattern(PyXBoxPattern, parsed, ignore_nl=False)
1100 if not m:
1101 raise TexResultError("PyXBox expected")
1102 if m.group("page") != str(self.page):
1103 raise TexResultError("Wrong page number in PyXBox")
1104 extent_pt = [float(x)*72/72.27 for x in m.group("lt", "rt", "ht", "dp")]
1105 parsed, m = remove_string("[80.121.88.%s]" % self.page, parsed)
1106 if not m:
1107 raise TexResultError("PyXPageOutMarker expected")
1108 else:
1109 # check for "Output written on ...dvi (1 page, 220 bytes)."
1110 if self.page:
1111 parsed, m = remove_pattern(dvi_pattern, parsed)
1112 if not m:
1113 raise TexResultError("TeX dvifile messages expected")
1114 if m.group("page") != str(self.page):
1115 raise TexResultError("wrong number of pages reported")
1116 else:
1117 parsed, m = remove_string("No pages of output.", parsed)
1118 if not m:
1119 raise TexResultError("no dvifile expected")
1121 for t in texmessages:
1122 parsed = t(parsed)
1123 if parsed.replace(r"(Please type a command or say `\end')", "").replace(" ", "").replace("*\n", "").replace("\n", ""):
1124 raise TexResultError("unhandled TeX response (might be an error)")
1125 except TexResultError as e:
1126 if self.errordetail > errordetail.none:
1127 def add(msg): e.args = (e.args[0] + msg,)
1128 add("\nThe expression passed to TeX was:\n{}".format(indent_text(expr.rstrip())))
1129 if self.errordetail == errordetail.full:
1130 add("\nThe return message from TeX was:\n{}".format(indent_text(unparsed.rstrip())))
1131 if self.errordetail == errordetail.default:
1132 if parsed.count('\n') > 6:
1133 parsed = "\n".join(parsed.split("\n")[:5] + ["(cut after 5 lines; use errordetail.full for all output)"])
1134 add("\nAfter parsing the return message from TeX, the following was left:\n{}".format(indent_text(parsed.rstrip())))
1135 raise e
1136 if oldstate == newstate == STATE_TYPESET:
1137 return extent_pt
1139 def do_start(self):
1140 """Setup environment and start TeX interpreter."""
1141 assert self.state == STATE_START
1142 self.state = STATE_PREAMBLE
1144 chroot = config.get("text", "chroot", "")
1145 if chroot:
1146 chroot_tmpdir = config.get("text", "tmpdir", "/tmp")
1147 chroot_tmpdir_rel = os.path.relpath(chroot_tmpdir, os.sep)
1148 base_tmpdir = os.path.join(chroot, chroot_tmpdir_rel)
1149 else:
1150 base_tmpdir = config.get("text", "tmpdir", None)
1151 self.tmpdir = tempfile.mkdtemp(prefix="pyx", dir=base_tmpdir)
1152 atexit.register(self._cleanup)
1153 for usefile in self.usefiles:
1154 extpos = usefile.rfind(".")
1155 try:
1156 os.rename(usefile, os.path.join(self.tmpdir, "texput" + usefile[extpos:]))
1157 except OSError:
1158 pass
1159 if chroot:
1160 tex_tmpdir = os.sep + os.path.relpath(self.tmpdir, chroot)
1161 else:
1162 tex_tmpdir = self.tmpdir
1163 cmd = self.cmd + ['--output-directory', tex_tmpdir]
1164 if self.texipc:
1165 cmd.append("--ipc")
1166 self.popen = config.Popen(cmd, stdin=config.PIPE, stdout=config.PIPE, stderr=config.STDOUT, bufsize=0)
1167 self.texinput = io.TextIOWrapper(self.popen.stdin, encoding=self.texenc)
1168 if self.copyinput:
1169 try:
1170 self.copyinput.write
1171 except AttributeError:
1172 self.texinput = Tee(open(self.copyinput, "w", encoding=self.texenc), self.texinput)
1173 else:
1174 self.texinput = Tee(self.copyinput, self.texinput)
1175 self.texoutput = MonitorOutput(self.name, io.TextIOWrapper(self.popen.stdout, encoding=self.texenc))
1176 self._execute("\\scrollmode\n\\raiseerror%\n" # switch to and check scrollmode
1177 "\\def\\PyX{P\\kern-.3em\\lower.5ex\hbox{Y}\kern-.18em X}%\n" # just the PyX Logo
1178 "\\gdef\\PyXBoxHAlign{0}%\n" # global PyXBoxHAlign (0.0-1.0) for the horizontal alignment, default to 0
1179 "\\newbox\\PyXBox%\n" # PyXBox will contain the output
1180 "\\newbox\\PyXBoxHAligned%\n" # PyXBox will contain the horizontal aligned output
1181 "\\newdimen\\PyXDimenHAlignLT%\n" # PyXDimenHAlignLT/RT will contain the left/right extent
1182 "\\newdimen\\PyXDimenHAlignRT%\n" +
1183 _textattrspreamble + # insert preambles for textattrs macros
1184 "\\long\\def\\ProcessPyXBox#1#2{%\n" # the ProcessPyXBox definition (#1 is expr, #2 is page number)
1185 "\\setbox\\PyXBox=\\hbox{{#1}}%\n" # push expression into PyXBox
1186 "\\PyXDimenHAlignLT=\\PyXBoxHAlign\\wd\\PyXBox%\n" # calculate the left/right extent
1187 "\\PyXDimenHAlignRT=\\wd\\PyXBox%\n"
1188 "\\advance\\PyXDimenHAlignRT by -\\PyXDimenHAlignLT%\n"
1189 "\\gdef\\PyXBoxHAlign{0}%\n" # reset the PyXBoxHAlign to the default 0
1190 "\\immediate\\write16{PyXBox:page=#2," # write page and extents of this box to stdout
1191 "lt=\\the\\PyXDimenHAlignLT,"
1192 "rt=\\the\\PyXDimenHAlignRT,"
1193 "ht=\\the\\ht\\PyXBox,"
1194 "dp=\\the\\dp\\PyXBox:}%\n"
1195 "\\setbox\\PyXBoxHAligned=\\hbox{\\kern-\\PyXDimenHAlignLT\\box\\PyXBox}%\n" # align horizontally
1196 "\\ht\\PyXBoxHAligned0pt%\n" # baseline alignment (hight to zero)
1197 "{\\count0=80\\count1=121\\count2=88\\count3=#2\\shipout\\box\\PyXBoxHAligned}}%\n" # shipout PyXBox to Page 80.121.88.<page number>
1198 "\\def\\PyXInput#1{\\immediate\\write16{PyXInputMarker:executeid=#1:}}%\n" # write PyXInputMarker to stdout
1199 "\\def\\PyXMarker#1{\\hskip0pt\\special{PyX:marker #1}}%", # write PyXMarker special into the dvi-file
1200 self.texmessages_start_default + self.texmessages_start, STATE_PREAMBLE, STATE_PREAMBLE)
1202 def do_preamble(self, expr, texmessages):
1203 """Ensure preamble mode and execute expr."""
1204 if self.state < STATE_PREAMBLE:
1205 self.do_start()
1206 self._execute(expr, texmessages, STATE_PREAMBLE, STATE_PREAMBLE)
1208 def do_typeset(self, expr, texmessages):
1209 """Ensure typeset mode and typeset expr."""
1210 if self.state < STATE_PREAMBLE:
1211 self.do_start()
1212 if self.state < STATE_TYPESET:
1213 self.go_typeset()
1214 return self._execute(expr, texmessages, STATE_TYPESET, STATE_TYPESET)
1216 def do_finish(self):
1217 """Teardown TeX interpreter and cleanup environment."""
1218 if self.state == STATE_DONE:
1219 return
1220 if self.state < STATE_TYPESET:
1221 self.go_typeset()
1222 self.go_finish()
1223 assert self.state == STATE_DONE
1224 self.texinput.close() # close the input queue and
1225 self.texoutput.done() # wait for finish of the output
1227 if self.needdvitextboxes:
1228 dvifilename = os.path.join(self.tmpdir, "texput.dvi")
1229 self.dvifile = dvifile.DVIfile(dvifilename, debug=self.dvitype)
1230 page = 1
1231 for box in self.needdvitextboxes:
1232 box.readdvipage(self.dvifile, page)
1233 page += 1
1234 if self.dvifile is not None and self.dvifile.readpage(None) is not None:
1235 raise ValueError("end of dvifile expected but further pages follow")
1237 atexit.unregister(self._cleanup)
1238 self._cleanup()
1240 def preamble(self, expr, texmessages=[]):
1241 """Execute a preamble.
1243 :param str expr: expression to be executed
1244 :param texmessages: additional message parsers
1245 :type texmessages: list of :class:`texmessage` parsers
1247 Preambles must not generate output, but are used to load files, perform
1248 settings, define macros, *etc*. In LaTeX mode, preambles are executed
1249 before ``\\begin{document}``. The method can be called multiple times,
1250 but only prior to :meth:`SingleRunner.text` and
1251 :meth:`SingleRunner.text_pt`.
1254 texmessages = self.texmessages_preamble_default + self.texmessages_preamble + texmessages
1255 self.do_preamble(expr, texmessages)
1257 def text_pt(self, x_pt, y_pt, expr, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1258 """Typeset text.
1260 :param float x_pt: x position in pts
1261 :param float y_pt: y position in pts
1262 :param str expr: text to be typeset
1263 :param textattrs: styles and attributes to be applied to the text
1264 :type textattrs: list of :class:`textattr, :class:`trafo.trafo_pt`,
1265 and :class:`style.fillstyle`
1266 :param texmessages: additional message parsers
1267 :type texmessages: list of :class:`texmessage` parsers
1268 :param fontmap: force a fontmap to be used (instead of the default
1269 depending on the output format)
1270 :type fontmap: None or fontmap
1271 :param bool singlecharmode: position each character separately
1272 :returns: text output insertable into a canvas.
1273 :rtype: :class:`textextbox_pt`
1274 :raises: :exc:`TexDoneError`: when the TeX interpreter has been
1275 terminated already.
1278 if self.state == STATE_DONE:
1279 raise TexDoneError("typesetting process was terminated already")
1280 textattrs = attr.mergeattrs(textattrs) # perform cleans
1281 attr.checkattrs(textattrs, [textattr, trafo.trafo_pt, style.fillstyle])
1282 trafos = attr.getattrs(textattrs, [trafo.trafo_pt])
1283 fillstyles = attr.getattrs(textattrs, [style.fillstyle])
1284 textattrs = attr.getattrs(textattrs, [textattr])
1285 for ta in textattrs[::-1]:
1286 expr = ta.apply(expr)
1287 first = self.state < STATE_TYPESET
1288 left_pt, right_pt, height_pt, depth_pt = self.do_typeset(expr, self.texmessages_run_default + self.texmessages_run + texmessages)
1289 if self.texipc and first:
1290 self.dvifile = dvifile.DVIfile(os.path.join(self.tmpdir, "texput.dvi"), debug=self.dvitype)
1291 box = textextbox_pt(x_pt, y_pt, left_pt, right_pt, height_pt, depth_pt, self.do_finish, fontmap, singlecharmode, fillstyles)
1292 for t in trafos:
1293 box.reltransform(t) # TODO: should trafos really use reltransform???
1294 # this is quite different from what we do elsewhere!!!
1295 # see https://sourceforge.net/mailarchive/forum.php?thread_id=9137692&forum_id=23700
1296 if self.texipc:
1297 box.readdvipage(self.dvifile, self.page)
1298 else:
1299 self.needdvitextboxes.append(box)
1300 return box
1302 def text(self, x, y, *args, **kwargs):
1303 """Typeset text.
1305 This method is identical to :meth:`text_pt` with the only difference of
1306 using PyX lengths to position the output.
1308 :param x: x position
1309 :type x: PyX length
1310 :param y: y position
1311 :type y: PyX length
1314 return self.text_pt(unit.topt(x), unit.topt(y), *args, **kwargs)
1317 class SingleTexRunner(SingleRunner):
1319 def __init__(self, cmd=config.getlist("text", "tex", ["tex"]), lfs="10pt", **kwargs):
1320 """Plain TeX interface.
1322 This class adjusts the :class:`SingleRunner` to use plain TeX.
1324 :param cmd: command and arguments to start the TeX interpreter
1325 :type cmd: list of str
1326 :param lfs: resemble LaTeX font settings within plain TeX by loading a
1327 lfs-file
1328 :type lfs: str or None
1329 :param kwargs: additional arguments passed to :class:`SingleRunner`
1331 An lfs-file is a file defining a set of font commands like ``\\normalsize``
1332 by font selection commands in plain TeX. Several of those files
1333 resembling standard settings of LaTeX are distributed along with PyX in
1334 the ``pyx/data/lfs`` directory. This directory also contains a LaTeX
1335 file to create lfs files for different settings (LaTeX class, class
1336 options, and style files).
1339 super().__init__(cmd=cmd, **kwargs)
1340 self.lfs = lfs
1341 self.name = "TeX"
1343 def go_typeset(self):
1344 assert self.state == STATE_PREAMBLE
1345 self.state = STATE_TYPESET
1347 def go_finish(self):
1348 self._execute("\\end%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1350 def force_done(self):
1351 self.texinput.write("\n\\end\n")
1353 def do_start(self):
1354 super().do_start()
1355 if self.lfs:
1356 if not self.lfs.endswith(".lfs"):
1357 self.lfs = "%s.lfs" % self.lfs
1358 with config.open(self.lfs, []) as lfsfile:
1359 lfsdef = lfsfile.read().decode("ascii")
1360 self._execute(lfsdef, [], STATE_PREAMBLE, STATE_PREAMBLE)
1361 self._execute("\\normalsize%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1362 self._execute("\\newdimen\\linewidth\\newdimen\\textwidth%\n", [], STATE_PREAMBLE, STATE_PREAMBLE)
1365 class SingleLatexRunner(SingleRunner):
1367 #: default :class:`texmessage` parsers at LaTeX class loading
1368 texmessages_docclass_default = [texmessage.load]
1369 #: default :class:`texmessage` parsers at ``\begin{document}``
1370 texmessages_begindoc_default = [texmessage.load, texmessage.no_aux]
1372 def __init__(self, cmd=config.getlist("text", "latex", ["latex"]),
1373 docclass="article", docopt=None, pyxgraphics=True,
1374 texmessages_docclass=[], texmessages_begindoc=[], **kwargs):
1375 """LaTeX interface.
1377 This class adjusts the :class:`SingleRunner` to use LaTeX.
1379 :param cmd: command and arguments to start the TeX interpreter
1380 in LaTeX mode
1381 :type cmd: list of str
1382 :param str docclass: document class
1383 :param docopt: document loading options
1384 :type docopt: str or None
1385 :param bool pyxgraphics: activate graphics bundle support, see
1386 :ref:`pyxgraphics`
1387 :param texmessages_docclass: additional message parsers at LaTeX class
1388 loading
1389 :type texmessages_docclass: list of :class:`texmessage` parsers
1390 :param texmessages_begindoc: additional message parsers at
1391 ``\\begin{document}``
1392 :type texmessages_begindoc: list of :class:`texmessage` parsers
1393 :param kwargs: additional arguments passed to :class:`SingleRunner`
1396 super().__init__(cmd=cmd, **kwargs)
1397 self.docclass = docclass
1398 self.docopt = docopt
1399 self.pyxgraphics = pyxgraphics
1400 self.texmessages_docclass = texmessages_docclass
1401 self.texmessages_begindoc = texmessages_begindoc
1402 self.name = "LaTeX"
1404 def go_typeset(self):
1405 self._execute("\\begin{document}", self.texmessages_begindoc_default + self.texmessages_begindoc, STATE_PREAMBLE, STATE_TYPESET)
1407 def go_finish(self):
1408 self._execute("\\end{document}%\n", self.texmessages_end_default + self.texmessages_end, STATE_TYPESET, STATE_DONE)
1410 def force_done(self):
1411 self.texinput.write("\n\\catcode`\\@11\\relax\\@@end\n")
1413 def do_start(self):
1414 super().do_start()
1415 if self.pyxgraphics:
1416 with config.open("pyx.def", []) as source, open(os.path.join(self.tmpdir, "pyx.def"), "wb") as dest:
1417 dest.write(source.read())
1418 self._execute("\\makeatletter%\n"
1419 "\\let\\saveProcessOptions=\\ProcessOptions%\n"
1420 "\\def\\ProcessOptions{%\n"
1421 "\\def\\Gin@driver{" + self.tmpdir.replace(os.sep, "/") + "/pyx.def}%\n"
1422 "\\def\\c@lor@namefile{dvipsnam.def}%\n"
1423 "\\saveProcessOptions}%\n"
1424 "\\makeatother",
1425 [], STATE_PREAMBLE, STATE_PREAMBLE)
1426 if self.docopt is not None:
1427 self._execute("\\documentclass[%s]{%s}" % (self.docopt, self.docclass),
1428 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1429 else:
1430 self._execute("\\documentclass{%s}" % self.docclass,
1431 self.texmessages_docclass_default + self.texmessages_docclass, STATE_PREAMBLE, STATE_PREAMBLE)
1434 def reset_for_tex_done(f):
1435 @functools.wraps(f)
1436 def wrapped(self, *args, **kwargs):
1437 try:
1438 return f(self, *args, **kwargs)
1439 except TexDoneError:
1440 self.reset(reinit=True)
1441 return f(self, *args, **kwargs)
1442 return wrapped
1445 class MultiRunner:
1447 def __init__(self, cls, *args, **kwargs):
1448 """A restartable :class:`SingleRunner` class
1450 :param cls: the class being wrapped
1451 :type cls: :class:`SingleRunner` class
1452 :param list args: args at class instantiation
1453 :param dict kwargs: keyword args at at class instantiation
1456 self.cls = cls
1457 self.args = args
1458 self.kwargs = kwargs
1459 self.reset()
1461 def preamble(self, expr, texmessages=[]):
1462 "resembles :meth:`SingleRunner.preamble`"
1463 self.preambles.append((expr, texmessages))
1464 self.instance.preamble(expr, texmessages)
1466 @reset_for_tex_done
1467 def text_pt(self, *args, **kwargs):
1468 "resembles :meth:`SingleRunner.text_pt`"
1469 return self.instance.text_pt(*args, **kwargs)
1471 @reset_for_tex_done
1472 def text(self, *args, **kwargs):
1473 "resembles :meth:`SingleRunner.text`"
1474 return self.instance.text(*args, **kwargs)
1476 def reset(self, reinit=False):
1477 """Start a new :class:`SingleRunner` instance
1479 :param bool reinit: replay :meth:`preamble` calls on the new instance
1481 After executing this function further preamble calls are allowed,
1482 whereas once a text output has been created, :meth:`preamble` calls are
1483 forbidden.
1486 self.instance = self.cls(*self.args, **self.kwargs)
1487 if reinit:
1488 for expr, texmessages in self.preambles:
1489 self.instance.preamble(expr, texmessages)
1490 else:
1491 self.preambles = []
1494 class TexRunner(MultiRunner):
1496 def __init__(self, *args, **kwargs):
1497 """A restartable :class:`SingleTexRunner` class
1499 :param list args: args at class instantiation
1500 :param dict kwargs: keyword args at at class instantiation
1503 super().__init__(SingleTexRunner, *args, **kwargs)
1506 class LatexRunner(MultiRunner):
1508 def __init__(self, *args, **kwargs):
1509 """A restartable :class:`SingleLatexRunner` class
1511 :param list args: args at class instantiation
1512 :param dict kwargs: keyword args at at class instantiation
1515 super().__init__(SingleLatexRunner, *args, **kwargs)
1518 from pyx.font import T1font
1519 from pyx.font.t1file import from_PF_bytes
1520 from pyx.font.afmfile import AFMfile
1522 class unicodetextbox_pt(textbox_pt):
1524 def __init__(self, x_pt, y_pt, text, font, size):
1525 self.text = text
1526 self.font = font
1527 self.size = size
1528 self.texttrafo = trafo.scale(unit.scale["x"]).translated_pt(x_pt, y_pt)
1529 bbox = self.font.text_pt(0, 0, text, size).bbox()
1530 box.rect_pt.__init__(self, -bbox.llx_pt, -bbox.lly_pt, -bbox.llx_pt+bbox.urx_pt, -bbox.lly_pt+bbox.ury_pt, abscenter_pt = (-bbox.llx_pt, -bbox.lly_pt))
1531 box.rect.transform(self, self.texttrafo)
1533 def transform(self, *trafos):
1534 box.rect.transform(self, *trafos)
1535 for trafo in trafos:
1536 self.texttrafo = trafo * self.texttrafo
1538 def bbox(self):
1539 scale, x_pt, y_pt = self.homothety()
1540 return self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).bbox()
1542 def homothety(self):
1543 assert self.texttrafo.matrix[0][0] == self.texttrafo.matrix[1][1]
1544 assert self.texttrafo.matrix[0][1] == 0
1545 assert self.texttrafo.matrix[1][0] == 0
1546 return self.texttrafo.matrix[0][0], self.texttrafo.vector[0], self.texttrafo.vector[1]
1548 def textpath(self):
1549 scale, x_pt, y_pt = self.homothety()
1550 return self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).textpath()
1552 def requiretextregion(self):
1553 return True
1555 def processPS(self, file, writer, context, registry, bbox):
1556 scale, x_pt, y_pt = self.homothety()
1557 self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processPS(file, writer, context, registry, bbox)
1559 def processPDF(self, file, writer, context, registry, bbox):
1560 scale, x_pt, y_pt = self.homothety()
1561 self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processPDF(file, writer, context, registry, bbox)
1563 def processSVG(self, xml, writer, context, registry, bbox):
1564 scale, x_pt, y_pt = self.homothety()
1565 self.font.text_pt(x_pt, y_pt, self.text, scale*self.size).processSVG(xml, writer, context, registry, bbox)
1568 class UnicodeText:
1570 def __init__(self, fontname="cmr10", size=10):
1571 self.font = T1font(from_PF_bytes(config.open(fontname, [config.format.type1]).read()),
1572 AFMfile(config.open(fontname, [config.format.afm], ascii=True)))
1573 self.size = size
1575 def preamble(self):
1576 raise NotImplemented()
1578 def reset(self):
1579 raise NotImplemented()
1581 def text_pt(self, x_pt, y_pt, text, textattrs=[], texmessages=[], fontmap=None, singlecharmode=False):
1582 # def text_pt(self, x_pt, y_pt, text, *args, **kwargs):
1583 return unicodetextbox_pt(x_pt, y_pt, text, self.font, self.size)
1585 def text(self, x, y, *args, **kwargs):
1586 return self.text_pt(unit.topt(x), unit.topt(y), *args, **kwargs)
1589 # old, deprecated names:
1590 texrunner = TexRunner
1591 latexrunner = LatexRunner
1593 # module level interface documentation for autodoc
1594 # the actual values are setup by the set function
1596 #: the current :class:`MultiRunner` instance for the module level functions
1597 default_runner = None
1599 #: default_runner.preamble (bound method)
1600 preamble = None
1602 #: default_runner.text_pt (bound method)
1603 text_pt = None
1605 #: default_runner.text (bound method)
1606 text = None
1608 #: default_runner.reset (bound method)
1609 reset = None
1611 def set(cls=TexRunner, mode=None, *args, **kwargs):
1612 """Setup a new module level :class:`MultiRunner`
1614 :param cls: the module level :class:`MultiRunner` to be used, i.e.
1615 :class:`TexRunner` or :class:`LatexRunner`
1616 :type cls: :class:`MultiRunner` object, not instance
1617 :param mode: ``"tex"`` for :class:`TexRunner` or ``"latex"`` for
1618 :class:`LatexRunner` with arbitraty capitalization, overwriting the cls
1619 value
1621 :deprecated: use the cls argument instead
1622 :type mode: str or None
1623 :param list args: args at class instantiation
1624 :param dict kwargs: keyword args at at class instantiation
1627 # note: defaulttexrunner is deprecated
1628 global default_runner, defaulttexrunner, reset, preamble, text, text_pt
1629 if mode is not None:
1630 logger.warning("mode setting is deprecated, use the cls argument instead")
1631 cls = {"tex": TexRunner, "latex": LatexRunner}[mode.lower()]
1632 default_runner = defaulttexrunner = cls(*args, **kwargs)
1633 preamble = default_runner.preamble
1634 text_pt = default_runner.text_pt
1635 text = default_runner.text
1636 reset = default_runner.reset
1638 # initialize default_runner
1639 set()
1642 def escapestring(s, replace={" ": "~",
1643 "$": "\\$",
1644 "&": "\\&",
1645 "#": "\\#",
1646 "_": "\\_",
1647 "%": "\\%",
1648 "^": "\\string^",
1649 "~": "\\string~",
1650 "<": "{$<$}",
1651 ">": "{$>$}",
1652 "{": "{$\{$}",
1653 "}": "{$\}$}",
1654 "\\": "{$\setminus$}",
1655 "|": "{$\mid$}"}):
1656 "Escapes ASCII characters such that they can be typeset by TeX/LaTeX"""
1657 i = 0
1658 while i < len(s):
1659 if not 32 <= ord(s[i]) < 127:
1660 raise ValueError("escapestring function handles ascii strings only")
1661 c = s[i]
1662 try:
1663 r = replace[c]
1664 except KeyError:
1665 i += 1
1666 else:
1667 s = s[:i] + r + s[i+1:]
1668 i += len(r)
1669 return s