##// END OF EJS Templates
fix coding style with darker
yuji96 -
Show More
@@ -1,666 +1,668 b''
1 1 """Various display related classes.
2 2
3 3 Authors : MinRK, gregcaporaso, dannystaple
4 4 """
5 5 from html import escape as html_escape
6 6 from os.path import exists, isfile, splitext, abspath, join, isdir
7 7 from os import walk, sep, fsdecode
8 8
9 9 from IPython.core.display import DisplayObject, TextDisplayObject
10 10
11 11 from typing import Tuple
12 12
13 13 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
14 14 'FileLink', 'FileLinks', 'Code']
15 15
16 16
17 17 class Audio(DisplayObject):
18 18 """Create an audio object.
19 19
20 20 When this object is returned by an input cell or passed to the
21 21 display function, it will result in Audio controls being displayed
22 22 in the frontend (only works in the notebook).
23 23
24 24 Parameters
25 25 ----------
26 26 data : numpy array, list, unicode, str or bytes
27 27 Can be one of
28 28
29 29 * Numpy 1d array containing the desired waveform (mono)
30 30 * Numpy 2d array containing waveforms for each channel.
31 31 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
32 32 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
33 33 * List of float or integer representing the waveform (mono)
34 34 * String containing the filename
35 35 * Bytestring containing raw PCM data or
36 36 * URL pointing to a file on the web.
37 37
38 38 If the array option is used, the waveform will be normalized.
39 39
40 40 If a filename or url is used, the format support will be browser
41 41 dependent.
42 42 url : unicode
43 43 A URL to download the data from.
44 44 filename : unicode
45 45 Path to a local file to load the data from.
46 46 embed : boolean
47 47 Should the audio data be embedded using a data URI (True) or should
48 48 the original source be referenced. Set this to True if you want the
49 49 audio to playable later with no internet connection in the notebook.
50 50
51 51 Default is `True`, unless the keyword argument `url` is set, then
52 52 default value is `False`.
53 53 rate : integer
54 54 The sampling rate of the raw data.
55 55 Only required when data parameter is being used as an array
56 56 autoplay : bool
57 57 Set to True if the audio should immediately start playing.
58 58 Default is `False`.
59 59 normalize : bool
60 60 Whether audio should be normalized (rescaled) to the maximum possible
61 61 range. Default is `True`. When set to `False`, `data` must be between
62 62 -1 and 1 (inclusive), otherwise an error is raised.
63 63 Applies only when `data` is a list or array of samples; other types of
64 64 audio are never normalized.
65 65
66 66 Examples
67 67 --------
68 68
69 69 Generate a sound
70 70
71 71 >>> import numpy as np
72 72 ... framerate = 44100
73 73 ... t = np.linspace(0,5,framerate*5)
74 74 ... data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
75 75 ... Audio(data, rate=framerate)
76 76
77 77 Can also do stereo or more channels
78 78
79 79 >>> dataleft = np.sin(2*np.pi*220*t)
80 80 ... dataright = np.sin(2*np.pi*224*t)
81 81 ... Audio([dataleft, dataright], rate=framerate)
82 82
83 83 From URL:
84 84
85 85 >>> Audio("http://www.nch.com.au/acm/8k16bitpcm.wav")
86 86 >>> Audio(url="http://www.w3schools.com/html/horse.ogg")
87 87
88 88 From a File:
89 89
90 90 >>> Audio('/path/to/sound.wav')
91 91 >>> Audio(filename='/path/to/sound.ogg')
92 92
93 93 From Bytes:
94 94
95 95 >>> Audio(b'RAW_WAV_DATA..')
96 96 >>> Audio(data=b'RAW_WAV_DATA..')
97 97
98 98 See Also
99 99 --------
100 100 ipywidgets.Audio
101 101
102 102 AUdio widget with more more flexibility and options.
103 103
104 104 """
105 105 _read_flags = 'rb'
106 106
107 107 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True, *,
108 108 element_id=None):
109 109 if filename is None and url is None and data is None:
110 110 raise ValueError("No audio data found. Expecting filename, url, or data.")
111 111 if embed is False and url is None:
112 112 raise ValueError("No url found. Expecting url when embed=False")
113 113
114 114 if url is not None and embed is not True:
115 115 self.embed = False
116 116 else:
117 117 self.embed = True
118 118 self.autoplay = autoplay
119 119 self.element_id = element_id
120 120 super(Audio, self).__init__(data=data, url=url, filename=filename)
121 121
122 122 if self.data is not None and not isinstance(self.data, bytes):
123 123 if rate is None:
124 124 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
125 125 self.data = Audio._make_wav(data, rate, normalize)
126 126
127 127 def reload(self):
128 128 """Reload the raw data from file or URL."""
129 129 import mimetypes
130 130 if self.embed:
131 131 super(Audio, self).reload()
132 132
133 133 if self.filename is not None:
134 134 self.mimetype = mimetypes.guess_type(self.filename)[0]
135 135 elif self.url is not None:
136 136 self.mimetype = mimetypes.guess_type(self.url)[0]
137 137 else:
138 138 self.mimetype = "audio/wav"
139 139
140 140 @staticmethod
141 141 def _make_wav(data, rate, normalize):
142 142 """ Transform a numpy array to a PCM bytestring """
143 143 from io import BytesIO
144 144 import wave
145 145
146 146 try:
147 147 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
148 148 except ImportError:
149 149 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
150 150
151 151 fp = BytesIO()
152 152 waveobj = wave.open(fp,mode='wb')
153 153 waveobj.setnchannels(nchan)
154 154 waveobj.setframerate(rate)
155 155 waveobj.setsampwidth(2)
156 156 waveobj.setcomptype('NONE','NONE')
157 157 waveobj.writeframes(scaled)
158 158 val = fp.getvalue()
159 159 waveobj.close()
160 160
161 161 return val
162 162
163 163 @staticmethod
164 164 def _validate_and_normalize_with_numpy(data, normalize) -> Tuple[bytes, int]:
165 165 import numpy as np
166 166
167 167 data = np.array(data, dtype=float)
168 168 if len(data.shape) == 1:
169 169 nchan = 1
170 170 elif len(data.shape) == 2:
171 171 # In wave files,channels are interleaved. E.g.,
172 172 # "L1R1L2R2..." for stereo. See
173 173 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
174 174 # for channel ordering
175 175 nchan = data.shape[0]
176 176 data = data.T.ravel()
177 177 else:
178 178 raise ValueError('Array audio input must be a 1D or 2D array')
179 179
180 180 max_abs_value = np.max(np.abs(data))
181 181 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
182 182 scaled = data / normalization_factor * 32767
183 183 return scaled.astype("<h").tobytes(), nchan
184 184
185 185 @staticmethod
186 186 def _validate_and_normalize_without_numpy(data, normalize):
187 187 import array
188 188 import sys
189 189
190 190 data = array.array('f', data)
191 191
192 192 try:
193 193 max_abs_value = float(max([abs(x) for x in data]))
194 194 except TypeError as e:
195 195 raise TypeError('Only lists of mono audio are '
196 196 'supported if numpy is not installed') from e
197 197
198 198 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
199 199 scaled = array.array('h', [int(x / normalization_factor * 32767) for x in data])
200 200 if sys.byteorder == 'big':
201 201 scaled.byteswap()
202 202 nchan = 1
203 203 return scaled.tobytes(), nchan
204 204
205 205 @staticmethod
206 206 def _get_normalization_factor(max_abs_value, normalize):
207 207 if not normalize and max_abs_value > 1:
208 208 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
209 209 return max_abs_value if normalize else 1
210 210
211 211 def _data_and_metadata(self):
212 212 """shortcut for returning metadata with url information, if defined"""
213 213 md = {}
214 214 if self.url:
215 215 md['url'] = self.url
216 216 if md:
217 217 return self.data, md
218 218 else:
219 219 return self.data
220 220
221 221 def _repr_html_(self):
222 222 src = """
223 223 <audio {element_id} controls="controls" {autoplay}>
224 224 <source src="{src}" type="{type}" />
225 225 Your browser does not support the audio element.
226 226 </audio>
227 227 """
228 228 return src.format(src=self.src_attr(), type=self.mimetype, autoplay=self.autoplay_attr(),
229 229 element_id=self.element_id_attr())
230 230
231 231 def src_attr(self):
232 232 import base64
233 233 if self.embed and (self.data is not None):
234 234 data = base64=base64.b64encode(self.data).decode('ascii')
235 235 return """data:{type};base64,{base64}""".format(type=self.mimetype,
236 236 base64=data)
237 237 elif self.url is not None:
238 238 return self.url
239 239 else:
240 240 return ""
241 241
242 242 def autoplay_attr(self):
243 243 if(self.autoplay):
244 244 return 'autoplay="autoplay"'
245 245 else:
246 246 return ''
247 247
248 248 def element_id_attr(self):
249 249 if (self.element_id):
250 250 return 'id="{element_id}"'.format(element_id=self.element_id)
251 251 else:
252 252 return ''
253 253
254 254 class IFrame(object):
255 255 """
256 256 Generic class to embed an iframe in an IPython notebook
257 257 """
258 258
259 259 iframe = """
260 260 <iframe
261 261 width="{width}"
262 262 height="{height}"
263 263 src="{src}{params}"
264 264 frameborder="0"
265 265 allowfullscreen
266 266 {extra}
267 267 ></iframe>
268 268 """
269 269
270 270 def __init__(self, src, width, height, extra="", **kwargs):
271 271 self.src = src
272 272 self.width = width
273 273 self.height = height
274 274 self.extra = extra
275 275 self.params = kwargs
276 276
277 277 def _repr_html_(self):
278 278 """return the embed iframe"""
279 279 if self.params:
280 280 from urllib.parse import urlencode
281 281 params = "?" + urlencode(self.params)
282 282 else:
283 283 params = ""
284 return self.iframe.format(src=self.src,
285 width=self.width,
286 height=self.height,
287 params=params,
288 extra=self.extra)
284 return self.iframe.format(
285 src=self.src,
286 width=self.width,
287 height=self.height,
288 params=params,
289 extra=self.extra,
290 )
289 291
290 292
291 293 class YouTubeVideo(IFrame):
292 294 """Class for embedding a YouTube Video in an IPython session, based on its video id.
293 295
294 296 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
295 297 do::
296 298
297 299 vid = YouTubeVideo("foo")
298 300 display(vid)
299 301
300 302 To start from 30 seconds::
301 303
302 304 vid = YouTubeVideo("abc", start=30)
303 305 display(vid)
304 306
305 307 To calculate seconds from time as hours, minutes, seconds use
306 308 :class:`datetime.timedelta`::
307 309
308 310 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
309 311
310 312 Other parameters can be provided as documented at
311 313 https://developers.google.com/youtube/player_parameters#Parameters
312 314
313 315 When converting the notebook using nbconvert, a jpeg representation of the video
314 316 will be inserted in the document.
315 317 """
316 318
317 319 def __init__(self, id, width=400, height=300, allow_autoplay=False, **kwargs):
318 320 self.id=id
319 321 src = "https://www.youtube.com/embed/{0}".format(id)
320 322 if allow_autoplay:
321 323 kwargs.update(autoplay=1, extra='allow="autoplay"')
322 324 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
323 325
324 326 def _repr_jpeg_(self):
325 327 # Deferred import
326 328 from urllib.request import urlopen
327 329
328 330 try:
329 331 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
330 332 except IOError:
331 333 return None
332 334
333 335 class VimeoVideo(IFrame):
334 336 """
335 337 Class for embedding a Vimeo video in an IPython session, based on its video id.
336 338 """
337 339
338 340 def __init__(self, id, width=400, height=300, **kwargs):
339 341 src="https://player.vimeo.com/video/{0}".format(id)
340 342 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
341 343
342 344 class ScribdDocument(IFrame):
343 345 """
344 346 Class for embedding a Scribd document in an IPython session
345 347
346 348 Use the start_page params to specify a starting point in the document
347 349 Use the view_mode params to specify display type one off scroll | slideshow | book
348 350
349 351 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
350 352
351 353 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
352 354 """
353 355
354 356 def __init__(self, id, width=400, height=300, **kwargs):
355 357 src="https://www.scribd.com/embeds/{0}/content".format(id)
356 358 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
357 359
358 360 class FileLink(object):
359 361 """Class for embedding a local file link in an IPython session, based on path
360 362
361 363 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
362 364
363 365 you would do::
364 366
365 367 local_file = FileLink("my/data.txt")
366 368 display(local_file)
367 369
368 370 or in the HTML notebook, just::
369 371
370 372 FileLink("my/data.txt")
371 373 """
372 374
373 375 html_link_str = "<a href='%s' target='_blank'>%s</a>"
374 376
375 377 def __init__(self,
376 378 path,
377 379 url_prefix='',
378 380 result_html_prefix='',
379 381 result_html_suffix='<br>'):
380 382 """
381 383 Parameters
382 384 ----------
383 385 path : str
384 386 path to the file or directory that should be formatted
385 387 url_prefix : str
386 388 prefix to be prepended to all files to form a working link [default:
387 389 '']
388 390 result_html_prefix : str
389 391 text to append to beginning to link [default: '']
390 392 result_html_suffix : str
391 393 text to append at the end of link [default: '<br>']
392 394 """
393 395 if isdir(path):
394 396 raise ValueError("Cannot display a directory using FileLink. "
395 397 "Use FileLinks to display '%s'." % path)
396 398 self.path = fsdecode(path)
397 399 self.url_prefix = url_prefix
398 400 self.result_html_prefix = result_html_prefix
399 401 self.result_html_suffix = result_html_suffix
400 402
401 403 def _format_path(self):
402 404 fp = ''.join([self.url_prefix, html_escape(self.path)])
403 405 return ''.join([self.result_html_prefix,
404 406 self.html_link_str % \
405 407 (fp, html_escape(self.path, quote=False)),
406 408 self.result_html_suffix])
407 409
408 410 def _repr_html_(self):
409 411 """return html link to file
410 412 """
411 413 if not exists(self.path):
412 414 return ("Path (<tt>%s</tt>) doesn't exist. "
413 415 "It may still be in the process of "
414 416 "being generated, or you may have the "
415 417 "incorrect path." % self.path)
416 418
417 419 return self._format_path()
418 420
419 421 def __repr__(self):
420 422 """return absolute path to file
421 423 """
422 424 return abspath(self.path)
423 425
424 426 class FileLinks(FileLink):
425 427 """Class for embedding local file links in an IPython session, based on path
426 428
427 429 e.g. to embed links to files that were generated in the IPython notebook
428 430 under ``my/data``, you would do::
429 431
430 432 local_files = FileLinks("my/data")
431 433 display(local_files)
432 434
433 435 or in the HTML notebook, just::
434 436
435 437 FileLinks("my/data")
436 438 """
437 439 def __init__(self,
438 440 path,
439 441 url_prefix='',
440 442 included_suffixes=None,
441 443 result_html_prefix='',
442 444 result_html_suffix='<br>',
443 445 notebook_display_formatter=None,
444 446 terminal_display_formatter=None,
445 447 recursive=True):
446 448 """
447 449 See :class:`FileLink` for the ``path``, ``url_prefix``,
448 450 ``result_html_prefix`` and ``result_html_suffix`` parameters.
449 451
450 452 included_suffixes : list
451 453 Filename suffixes to include when formatting output [default: include
452 454 all files]
453 455
454 456 notebook_display_formatter : function
455 457 Used to format links for display in the notebook. See discussion of
456 458 formatter functions below.
457 459
458 460 terminal_display_formatter : function
459 461 Used to format links for display in the terminal. See discussion of
460 462 formatter functions below.
461 463
462 464 Formatter functions must be of the form::
463 465
464 466 f(dirname, fnames, included_suffixes)
465 467
466 468 dirname : str
467 469 The name of a directory
468 470 fnames : list
469 471 The files in that directory
470 472 included_suffixes : list
471 473 The file suffixes that should be included in the output (passing None
472 474 meansto include all suffixes in the output in the built-in formatters)
473 475 recursive : boolean
474 476 Whether to recurse into subdirectories. Default is True.
475 477
476 478 The function should return a list of lines that will be printed in the
477 479 notebook (if passing notebook_display_formatter) or the terminal (if
478 480 passing terminal_display_formatter). This function is iterated over for
479 481 each directory in self.path. Default formatters are in place, can be
480 482 passed here to support alternative formatting.
481 483
482 484 """
483 485 if isfile(path):
484 486 raise ValueError("Cannot display a file using FileLinks. "
485 487 "Use FileLink to display '%s'." % path)
486 488 self.included_suffixes = included_suffixes
487 489 # remove trailing slashes for more consistent output formatting
488 490 path = path.rstrip('/')
489 491
490 492 self.path = path
491 493 self.url_prefix = url_prefix
492 494 self.result_html_prefix = result_html_prefix
493 495 self.result_html_suffix = result_html_suffix
494 496
495 497 self.notebook_display_formatter = \
496 498 notebook_display_formatter or self._get_notebook_display_formatter()
497 499 self.terminal_display_formatter = \
498 500 terminal_display_formatter or self._get_terminal_display_formatter()
499 501
500 502 self.recursive = recursive
501 503
502 504 def _get_display_formatter(self,
503 505 dirname_output_format,
504 506 fname_output_format,
505 507 fp_format,
506 508 fp_cleaner=None):
507 509 """ generate built-in formatter function
508 510
509 511 this is used to define both the notebook and terminal built-in
510 512 formatters as they only differ by some wrapper text for each entry
511 513
512 514 dirname_output_format: string to use for formatting directory
513 515 names, dirname will be substituted for a single "%s" which
514 516 must appear in this string
515 517 fname_output_format: string to use for formatting file names,
516 518 if a single "%s" appears in the string, fname will be substituted
517 519 if two "%s" appear in the string, the path to fname will be
518 520 substituted for the first and fname will be substituted for the
519 521 second
520 522 fp_format: string to use for formatting filepaths, must contain
521 523 exactly two "%s" and the dirname will be substituted for the first
522 524 and fname will be substituted for the second
523 525 """
524 526 def f(dirname, fnames, included_suffixes=None):
525 527 result = []
526 528 # begin by figuring out which filenames, if any,
527 529 # are going to be displayed
528 530 display_fnames = []
529 531 for fname in fnames:
530 532 if (isfile(join(dirname,fname)) and
531 533 (included_suffixes is None or
532 534 splitext(fname)[1] in included_suffixes)):
533 535 display_fnames.append(fname)
534 536
535 537 if len(display_fnames) == 0:
536 538 # if there are no filenames to display, don't print anything
537 539 # (not even the directory name)
538 540 pass
539 541 else:
540 542 # otherwise print the formatted directory name followed by
541 543 # the formatted filenames
542 544 dirname_output_line = dirname_output_format % dirname
543 545 result.append(dirname_output_line)
544 546 for fname in display_fnames:
545 547 fp = fp_format % (dirname,fname)
546 548 if fp_cleaner is not None:
547 549 fp = fp_cleaner(fp)
548 550 try:
549 551 # output can include both a filepath and a filename...
550 552 fname_output_line = fname_output_format % (fp, fname)
551 553 except TypeError:
552 554 # ... or just a single filepath
553 555 fname_output_line = fname_output_format % fname
554 556 result.append(fname_output_line)
555 557 return result
556 558 return f
557 559
558 560 def _get_notebook_display_formatter(self,
559 561 spacer="&nbsp;&nbsp;"):
560 562 """ generate function to use for notebook formatting
561 563 """
562 564 dirname_output_format = \
563 565 self.result_html_prefix + "%s/" + self.result_html_suffix
564 566 fname_output_format = \
565 567 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
566 568 fp_format = self.url_prefix + '%s/%s'
567 569 if sep == "\\":
568 570 # Working on a platform where the path separator is "\", so
569 571 # must convert these to "/" for generating a URI
570 572 def fp_cleaner(fp):
571 573 # Replace all occurrences of backslash ("\") with a forward
572 574 # slash ("/") - this is necessary on windows when a path is
573 575 # provided as input, but we must link to a URI
574 576 return fp.replace('\\','/')
575 577 else:
576 578 fp_cleaner = None
577 579
578 580 return self._get_display_formatter(dirname_output_format,
579 581 fname_output_format,
580 582 fp_format,
581 583 fp_cleaner)
582 584
583 585 def _get_terminal_display_formatter(self,
584 586 spacer=" "):
585 587 """ generate function to use for terminal formatting
586 588 """
587 589 dirname_output_format = "%s/"
588 590 fname_output_format = spacer + "%s"
589 591 fp_format = '%s/%s'
590 592
591 593 return self._get_display_formatter(dirname_output_format,
592 594 fname_output_format,
593 595 fp_format)
594 596
595 597 def _format_path(self):
596 598 result_lines = []
597 599 if self.recursive:
598 600 walked_dir = list(walk(self.path))
599 601 else:
600 602 walked_dir = [next(walk(self.path))]
601 603 walked_dir.sort()
602 604 for dirname, subdirs, fnames in walked_dir:
603 605 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
604 606 return '\n'.join(result_lines)
605 607
606 608 def __repr__(self):
607 609 """return newline-separated absolute paths
608 610 """
609 611 result_lines = []
610 612 if self.recursive:
611 613 walked_dir = list(walk(self.path))
612 614 else:
613 615 walked_dir = [next(walk(self.path))]
614 616 walked_dir.sort()
615 617 for dirname, subdirs, fnames in walked_dir:
616 618 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
617 619 return '\n'.join(result_lines)
618 620
619 621
620 622 class Code(TextDisplayObject):
621 623 """Display syntax-highlighted source code.
622 624
623 625 This uses Pygments to highlight the code for HTML and Latex output.
624 626
625 627 Parameters
626 628 ----------
627 629 data : str
628 630 The code as a string
629 631 url : str
630 632 A URL to fetch the code from
631 633 filename : str
632 634 A local filename to load the code from
633 635 language : str
634 636 The short name of a Pygments lexer to use for highlighting.
635 637 If not specified, it will guess the lexer based on the filename
636 638 or the code. Available lexers: http://pygments.org/docs/lexers/
637 639 """
638 640 def __init__(self, data=None, url=None, filename=None, language=None):
639 641 self.language = language
640 642 super().__init__(data=data, url=url, filename=filename)
641 643
642 644 def _get_lexer(self):
643 645 if self.language:
644 646 from pygments.lexers import get_lexer_by_name
645 647 return get_lexer_by_name(self.language)
646 648 elif self.filename:
647 649 from pygments.lexers import get_lexer_for_filename
648 650 return get_lexer_for_filename(self.filename)
649 651 else:
650 652 from pygments.lexers import guess_lexer
651 653 return guess_lexer(self.data)
652 654
653 655 def __repr__(self):
654 656 return self.data
655 657
656 658 def _repr_html_(self):
657 659 from pygments import highlight
658 660 from pygments.formatters import HtmlFormatter
659 661 fmt = HtmlFormatter()
660 662 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
661 663 return style + highlight(self.data, self._get_lexer(), fmt)
662 664
663 665 def _repr_latex_(self):
664 666 from pygments import highlight
665 667 from pygments.formatters import LatexFormatter
666 668 return highlight(self.data, self._get_lexer(), LatexFormatter())
General Comments 0
You need to be logged in to leave comments. Login now