Coverage for moptipy / utils / html.py: 79%

75 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""The HTML text format driver.""" 

2 

3from io import TextIOBase 

4from typing import Final 

5 

6from moptipy.utils.formatted_string import ( 

7 NAN, 

8 NEGATIVE_INFINITY, 

9 NUMBER, 

10 POSITIVE_INFINITY, 

11 SPECIAL, 

12 TEXT, 

13) 

14from moptipy.utils.text_format import MODE_TABLE_HEADER, TextFormatDriver 

15 

16#: the default border style 

17_BORDER: Final[str] = "1pt solid black" 

18 

19#: special characters in HTML 

20SPECIAL_CHARS: Final[dict[str, str]] = { 

21 "\u2205": "∅", 

22 "\u221E": "∞", 

23 "-\u221E": "-∞", 

24 "inf": "∞", 

25 "-inf": "-∞", 

26 "nan": "∅", 

27 "\u03b1": "α", 

28 "\u2014": "—", 

29} 

30 

31 

32class HTML(TextFormatDriver): 

33 """The HTML text driver.""" 

34 

35 def begin_table_body(self, stream: TextIOBase, cols: str) -> None: 

36 """Write the beginning of the table body.""" 

37 stream.write( 

38 f'<table style="border:{_BORDER};border-collapse:collapse">') 

39 

40 def end_table_body(self, stream: TextIOBase, cols: str) -> None: 

41 """Write the ending of the table body.""" 

42 stream.write("</tbody></table>") 

43 

44 def begin_table_header(self, stream: TextIOBase, cols: str) -> None: 

45 """Begin the header of an HTML table.""" 

46 stream.write("<thead>") 

47 

48 def end_table_header(self, stream: TextIOBase, cols: str) -> None: 

49 """End the header of an HTML table.""" 

50 stream.write("</thead><tbody>") 

51 

52 def begin_table_row(self, stream: TextIOBase, cols: str, 

53 section_index: int, row_index: int, 

54 row_mode: int) -> None: 

55 """Begin a row in an HTML table.""" 

56 stream.write("<tr>") 

57 

58 def end_table_row(self, stream: TextIOBase, cols: str, 

59 section_index: int, row_index: int) -> None: 

60 """End a row in a HTML table.""" 

61 stream.write("</tr>") 

62 

63 def begin_table_cell(self, stream: TextIOBase, cols: str, 

64 section_index: int, row_index: int, 

65 col_index: int, cell_mode: int) -> None: 

66 """Begin an HTML table cell.""" 

67 style: str = cols[col_index] 

68 style = "center" if style == "c" \ 

69 else "left" if style == "l" else "right" 

70 style = f"padding:3pt;text-align:{style}" 

71 if col_index < (len(cols) - 1): 

72 style = f"{style};border-right:{_BORDER}" 

73 if row_index <= 0 <= section_index: 

74 style = f"{style};border-top:{_BORDER}" 

75 cell: str = "th" if (cell_mode == MODE_TABLE_HEADER) else "td" 

76 stream.write(f'<{cell} style="{style}">') # noqa: B028 

77 

78 def end_table_cell(self, stream: TextIOBase, cols: str, 

79 section_index: int, row_index: int, 

80 col_index: int, cell_mode: int) -> None: 

81 """End an HTML table cell.""" 

82 stream.write("</th>" if (cell_mode == MODE_TABLE_HEADER) else "</td>") 

83 

84 def text(self, stream: TextIOBase, text: str, bold: bool, italic: bool, 

85 code: bool, mode: int) -> None: 

86 """Print a text string.""" 

87 if len(text) <= 0: 

88 return 

89 styles: str = "" 

90 if bold: 

91 styles = "font-weight:bold;" 

92 if italic: 

93 styles += "font-style:italic;" 

94 if code: 

95 styles += "font-family:monospace;" 

96 if len(styles) > 0: 

97 stream.write(f'<span style="{styles[0:-1]}">') # noqa: B028 

98 

99 if mode == TEXT: 

100 stream.write(text) 

101 elif mode == NUMBER: 

102 i: int = text.find("e") 

103 if i < 0: 

104 i = text.find("E") 

105 if i > 0: 

106 stream.write(f"{text[:i]}&#xD7;10<sup>{text[i + 1:]}</sup>") 

107 else: 

108 stream.write(text) 

109 elif mode == NAN: 

110 stream.write("&#x2205;") 

111 elif mode == POSITIVE_INFINITY: 

112 stream.write("&#x221E;") 

113 elif mode == NEGATIVE_INFINITY: 

114 stream.write("-&#x221E;") 

115 elif mode == SPECIAL: 

116 s: Final[str] = str(text) 

117 if s not in SPECIAL_CHARS: 

118 raise ValueError(f"invalid special character: {s!r}") 

119 stream.write(SPECIAL_CHARS[s]) 

120 else: 

121 raise ValueError(f"invalid mode {mode} for text {text!r}.") 

122 

123 if len(styles) > 0: 

124 stream.write("</span>") 

125 

126 def __str__(self): 

127 """ 

128 Get the appropriate file suffix. 

129 

130 :returns: the file suffix 

131 :retval 'html': always 

132 """ 

133 return "html" 

134 

135 @staticmethod 

136 def instance() -> "HTML": 

137 """ 

138 Get the HTML format singleton instance. 

139 

140 :returns: the singleton instance of the HTML format 

141 """ 

142 attr: Final[str] = "_instance" 

143 func: Final = HTML.instance 

144 if not hasattr(func, attr): 

145 setattr(func, attr, HTML()) 

146 return getattr(func, attr)