2020# along with this program. If not, see
2121# <http://www.gnu.org/licenses/>.
2222
23- import io
2423import optparse
2524import re
2625import sys
26+ from typing import Dict , Iterator , List , Optional , Set , Tuple , Union
2727
2828import pkg_resources
2929
3030try :
3131 from collections import OrderedDict
3232except ImportError :
33- from ordereddict import OrderedDict
33+ from ordereddict import OrderedDict # type: ignore
3434
3535from ansi2html .style import SCHEME , get_styles
3636
115115
116116
117117class _State :
118- def __init__ (self ):
118+ def __init__ (self ) -> None :
119119 self .inside_span = False
120120 self .reset ()
121121
122- def reset (self ):
123- self .intensity = ANSI_INTENSITY_NORMAL
124- self .style = ANSI_STYLE_NORMAL
125- self .blink = ANSI_BLINK_OFF
126- self .underline = ANSI_UNDERLINE_OFF
127- self .crossedout = ANSI_CROSSED_OUT_OFF
128- self .visibility = ANSI_VISIBILITY_ON
129- self .foreground = (ANSI_FOREGROUND_DEFAULT , None )
130- self .background = (ANSI_BACKGROUND_DEFAULT , None )
131- self .negative = ANSI_NEGATIVE_OFF
132-
133- def adjust (self , ansi_code , parameter = None ):
122+ def reset (self ) -> None :
123+ self .intensity : int = ANSI_INTENSITY_NORMAL
124+ self .style : int = ANSI_STYLE_NORMAL
125+ self .blink : int = ANSI_BLINK_OFF
126+ self .underline : int = ANSI_UNDERLINE_OFF
127+ self .crossedout : int = ANSI_CROSSED_OUT_OFF
128+ self .visibility : int = ANSI_VISIBILITY_ON
129+ self .foreground : Tuple [ int , Optional [ int ]] = (ANSI_FOREGROUND_DEFAULT , None )
130+ self .background : Tuple [ int , Optional [ int ]] = (ANSI_BACKGROUND_DEFAULT , None )
131+ self .negative : int = ANSI_NEGATIVE_OFF
132+
133+ def adjust (self , ansi_code : int , parameter : Optional [ int ] = None ) -> None :
134134 if ansi_code in (
135135 ANSI_INTENSITY_INCREASED ,
136136 ANSI_INTENSITY_REDUCED ,
@@ -174,17 +174,21 @@ def adjust(self, ansi_code, parameter=None):
174174 elif ansi_code in (ANSI_NEGATIVE_ON , ANSI_NEGATIVE_OFF ):
175175 self .negative = ansi_code
176176
177- def to_css_classes (self ):
178- css_classes = []
177+ def to_css_classes (self ) -> List [ str ] :
178+ css_classes : List [ str ] = []
179179
180- def append_unless_default (output , value , default ) :
180+ def append_unless_default (output : List [ str ] , value : int , default : int ) -> None :
181181 if value != default :
182182 css_class = "ansi%d" % value
183183 output .append (css_class )
184184
185185 def append_color_unless_default (
186- output , color , default , negative , neg_css_class
187- ):
186+ output : List [str ],
187+ color : Tuple [int , Optional [int ]],
188+ default : int ,
189+ negative : bool ,
190+ neg_css_class : str ,
191+ ) -> None :
188192 value , parameter = color
189193 if value != default :
190194 prefix = "inv" if negative else "ansi"
@@ -222,17 +226,17 @@ def append_color_unless_default(
222226
223227
224228class OSC_Link :
225- def __init__ (self , url , text ) :
229+ def __init__ (self , url : str , text : str ) -> None :
226230 self .url = url
227231 self .text = text
228232
229233
230- def map_vt100_box_code (char ) :
234+ def map_vt100_box_code (char : str ) -> str :
231235 char_hex = hex (ord (char ))
232236 return VT100_BOX_CODES [char_hex ] if char_hex in VT100_BOX_CODES else char
233237
234238
235- def _needs_extra_newline (text ) :
239+ def _needs_extra_newline (text : str ) -> bool :
236240 if not text or text .endswith ("\n " ):
237241 return False
238242 return True
@@ -254,18 +258,18 @@ class Ansi2HTMLConverter:
254258
255259 def __init__ (
256260 self ,
257- latex = False ,
258- inline = False ,
259- dark_bg = True ,
260- line_wrap = True ,
261- font_size = "normal" ,
262- linkify = False ,
263- escaped = True ,
264- markup_lines = False ,
265- output_encoding = "utf-8" ,
266- scheme = "ansi2html" ,
267- title = "" ,
268- ):
261+ latex : bool = False ,
262+ inline : bool = False ,
263+ dark_bg : bool = True ,
264+ line_wrap : bool = True ,
265+ font_size : str = "normal" ,
266+ linkify : bool = False ,
267+ escaped : bool = True ,
268+ markup_lines : bool = False ,
269+ output_encoding : str = "utf-8" ,
270+ scheme : str = "ansi2html" ,
271+ title : str = "" ,
272+ ) -> None :
269273
270274 self .latex = latex
271275 self .inline = inline
@@ -278,7 +282,7 @@ def __init__(
278282 self .output_encoding = output_encoding
279283 self .scheme = scheme
280284 self .title = title
281- self ._attrs = None
285+ self ._attrs : Optional [ Dict [ str , Union [ bool , str , Set [ str ]]]] = None
282286 self .hyperref = False
283287
284288 if inline :
@@ -298,27 +302,27 @@ def __init__(
298302 )
299303 self .osc_link_re = re .compile ("\033 \\ ]8;;(.*?)\007 (.*?)\033 \\ ]8;;\007 " )
300304
301- def do_linkify (self , line ) :
305+ def do_linkify (self , line : str ) -> str :
302306 if not isinstance (line , str ):
303307 return line # If line is an object, e.g. OSC_Link, it
304308 # will be expanded to a string later
305309 if self .latex :
306310 return self .url_matcher .sub (r"\\url{\1}" , line )
307311 return self .url_matcher .sub (r'<a href="\1">\1</a>' , line )
308312
309- def handle_osc_links (self , part ) :
313+ def handle_osc_links (self , part : OSC_Link ) -> str :
310314 if self .latex :
311315 self .hyperref = True
312316 return """\\ href{%s}{%s}""" % (part .url , part .text )
313317 return """<a href="%s">%s</a>""" % (part .url , part .text )
314318
315- def apply_regex (self , ansi ) :
316- styles_used = set ()
317- parts = self ._apply_regex (ansi , styles_used )
318- parts = self ._collapse_cursor (parts )
319- parts = list (parts )
319+ def apply_regex (self , ansi : str ) -> Tuple [ str , Set [ str ]] :
320+ styles_used : Set [ str ] = set ()
321+ all_parts = self ._apply_regex (ansi , styles_used )
322+ no_cursor_parts = self ._collapse_cursor (all_parts )
323+ no_cursor_parts = list (no_cursor_parts )
320324
321- def _check_links (parts ) :
325+ def _check_links (parts : List [ Union [ str , OSC_Link ]]) -> Iterator [ str ] :
322326 for part in parts :
323327 if isinstance (part , str ):
324328 if self .linkify :
@@ -330,7 +334,7 @@ def _check_links(parts):
330334 else :
331335 yield part
332336
333- parts = list (_check_links (parts ))
337+ parts = list (_check_links (no_cursor_parts ))
334338 combined = "" .join (parts )
335339 if self .markup_lines and not self .latex :
336340 combined = "\n " .join (
@@ -341,7 +345,9 @@ def _check_links(parts):
341345 )
342346 return combined , styles_used
343347
344- def _apply_regex (self , ansi , styles_used ):
348+ def _apply_regex (
349+ self , ansi : str , styles_used : Set [str ]
350+ ) -> Iterator [Union [str , OSC_Link , CursorMoveUp ]]:
345351 if self .escaped :
346352 if (
347353 self .latex
@@ -358,7 +364,7 @@ def _apply_regex(self, ansi, styles_used):
358364 for pattern , special in specials .items ():
359365 ansi = ansi .replace (pattern , special )
360366
361- def _vt100_box_drawing ():
367+ def _vt100_box_drawing () -> Iterator [ str ] :
362368 last_end = 0 # the index of the last end of a code we've seen
363369 box_drawing_mode = False
364370 for match in self .vt100_box_codes_prog .finditer (ansi ):
@@ -374,7 +380,7 @@ def _vt100_box_drawing():
374380
375381 ansi = "" .join (_vt100_box_drawing ())
376382
377- def _osc_link (ansi ) :
383+ def _osc_link (ansi : str ) -> Iterator [ Union [ str , OSC_Link ]] :
378384 last_end = 0
379385 for match in self .osc_link_re .finditer (ansi ):
380386 trailer = ansi [last_end : match .start ()]
@@ -397,20 +403,23 @@ def _osc_link(ansi):
397403 else :
398404 yield "</span>"
399405
400- def _handle_ansi_code (self , ansi , styles_used , state ):
406+ def _handle_ansi_code (
407+ self , ansi : str , styles_used : Set [str ], state : _State
408+ ) -> Iterator [Union [str , CursorMoveUp ]]:
401409 last_end = 0 # the index of the last end of a code we've seen
402410 for match in self .ansi_codes_prog .finditer (ansi ):
403411 yield ansi [last_end : match .start ()]
404412 last_end = match .end ()
405413
414+ params : Union [str , List [int ]]
406415 params , command = match .groups ()
407416
408417 if command not in "mMA" :
409418 continue
410419
411420 # Special cursor-moving code. The only supported one.
412421 if command == "A" :
413- yield CursorMoveUp
422+ yield CursorMoveUp # type: ignore
414423 continue
415424
416425 try :
@@ -452,7 +461,7 @@ def _handle_ansi_code(self, ansi, styles_used, state):
452461
453462 if v in (ANSI_FOREGROUND_256 , ANSI_BACKGROUND_256 ):
454463 try :
455- parameter = params [i + 2 ]
464+ parameter : Optional [ int ] = params [i + 2 ]
456465 except IndexError :
457466 continue
458467 skip_after_index = i + 2
@@ -495,22 +504,24 @@ def _handle_ansi_code(self, ansi, styles_used, state):
495504 state .inside_span = True
496505 yield ansi [last_end :]
497506
498- def _collapse_cursor (self , parts ):
507+ def _collapse_cursor (
508+ self , parts : Iterator [Union [str , OSC_Link , CursorMoveUp ]]
509+ ) -> List [Union [str , OSC_Link ]]:
499510 """Act on any CursorMoveUp commands by deleting preceding tokens"""
500511
501- final_parts = []
512+ final_parts : List [ Union [ str , OSC_Link ]] = []
502513 for part in parts :
503514
504515 # Throw out empty string tokens ("")
505516 if not part :
506517 continue
507518
508519 # Go back, deleting every token in the last 'line'
509- if part == CursorMoveUp :
520+ if isinstance ( part , CursorMoveUp ) :
510521 if final_parts :
511522 final_parts .pop ()
512523
513- while final_parts and "\n " not in final_parts [- 1 ]:
524+ while final_parts and "\n " not in final_parts [- 1 ]: # type: ignore
514525 final_parts .pop ()
515526
516527 continue
@@ -520,7 +531,9 @@ def _collapse_cursor(self, parts):
520531
521532 return final_parts
522533
523- def prepare (self , ansi = "" , ensure_trailing_newline = False ):
534+ def prepare (
535+ self , ansi : str = "" , ensure_trailing_newline : bool = False
536+ ) -> Dict [str , Union [bool , str , Set [str ]]]:
524537 """Load the contents of 'ansi' into this object"""
525538
526539 body , styles = self .apply_regex (ansi )
@@ -538,29 +551,31 @@ def prepare(self, ansi="", ensure_trailing_newline=False):
538551
539552 return self ._attrs
540553
541- def attrs (self ):
554+ def attrs (self ) -> Dict [ str , Union [ bool , str , Set [ str ]]] :
542555 """Prepare attributes for the template"""
543556 if not self ._attrs :
544557 raise Exception ("Method .prepare not yet called." )
545558 return self ._attrs
546559
547- def convert (self , ansi , full = True , ensure_trailing_newline = False ):
560+ def convert (
561+ self , ansi : str , full : bool = True , ensure_trailing_newline : bool = False
562+ ) -> str :
548563 r"""
549564 :param ansi: ANSI sequence to convert.
550565 :param full: Whether to include the full HTML document or only the body.
551566 :param ensure_trailing_newline: Ensures that ``\n`` character is present at the end of the output.
552567 """
553568 attrs = self .prepare (ansi , ensure_trailing_newline = ensure_trailing_newline )
554569 if not full :
555- return attrs ["body" ]
570+ return attrs ["body" ] # type: ignore
556571 if self .latex :
557572 _template = _latex_template
558573 else :
559574 _template = _html_template
560575 all_styles = get_styles (self .dark_bg , self .line_wrap , self .scheme )
561576 backgrounds = all_styles [:6 ]
562577 used_styles = filter (
563- lambda e : e .klass .lstrip ("." ) in attrs ["styles" ], all_styles
578+ lambda e : e .klass .lstrip ("." ) in attrs ["styles" ], all_styles # type: ignore
564579 )
565580
566581 return _template % {
@@ -572,15 +587,15 @@ def convert(self, ansi, full=True, ensure_trailing_newline=False):
572587 "hyperref" : "\\ usepackage{hyperref}" if self .hyperref else "" ,
573588 }
574589
575- def produce_headers (self ):
590+ def produce_headers (self ) -> str :
576591 return '<style type="text/css">\n %(style)s\n </style>\n ' % {
577592 "style" : "\n " .join (
578593 map (str , get_styles (self .dark_bg , self .line_wrap , self .scheme ))
579594 )
580595 }
581596
582597
583- def main ():
598+ def main () -> None :
584599 """
585600 $ ls --color=always | ansi2html > directories.html
586601 $ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html
@@ -718,16 +733,7 @@ def main():
718733 title = opts .output_title ,
719734 )
720735
721- try :
722- sys .stdin = io .TextIOWrapper (sys .stdin .detach (), opts .input_encoding , "replace" )
723- except io .UnsupportedOperation :
724- # This only fails in the test suite...
725- pass
726-
727- def _read (input_bytes ):
728- return input_bytes
729-
730- def _print (output_unicode , end = "\n " ):
736+ def _print (output_unicode : str , end : str = "\n " ) -> None :
731737 if hasattr (sys .stdout , "buffer" ):
732738 output_bytes = (output_unicode + end ).encode (opts .output_encoding )
733739 sys .stdout .buffer .write (output_bytes )
0 commit comments