Coverage for moptipy / utils / latex.py: 80%

69 statements  

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

1"""The latex 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 TextFormatDriver 

15 

16#: the exponent prefix 

17_EPREFIX = r"\hspace*{0.15em}*\hspace*{0.1em}10\textsuperscript{" 

18 

19#: special characters in LaTeX 

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

21 "\u2205": r"$\emptyset$", 

22 "\u221E": r"$\infty$", 

23 "-\u221E": r"$-\infty$", 

24 "inf": r"$\infty$", 

25 "-inf": r"$-\infty$", 

26 "nan": r"$\emptyset$", 

27 "\u03b1": r"$\alpha$", 

28 "\u2014": "---", 

29} 

30 

31 

32class LaTeX(TextFormatDriver): 

33 r""" 

34 The LaTeX text driver. 

35 

36 >>> from io import StringIO 

37 >>> from moptipy.utils.formatted_string import FormattedStr 

38 >>> from moptipy.utils.table import Table 

39 >>> s = StringIO() 

40 >>> latex = LaTeX.instance() 

41 >>> print(str(latex)) 

42 tex 

43 >>> with Table(s, "lrc", latex) as t: 

44 ... with t.header() as header: 

45 ... with header.row() as h: 

46 ... h.cell(FormattedStr("1", bold=True)) 

47 ... h.cell(FormattedStr("2", code=True)) 

48 ... h.cell(FormattedStr("3", italic=True)) 

49 ... with t.section() as g: 

50 ... with g.row() as r: 

51 ... r.cell("a") 

52 ... r.cell("b") 

53 ... r.cell("c") 

54 ... with g.row() as r: 

55 ... r.cell("d") 

56 ... r.cell("e") 

57 ... r.cell("f") 

58 >>> print(f"'{s.getvalue()}'") 

59 '\begin{tabular}{lrc}% 

60 \hline% 

61 {\textbf{1}}&{\texttt{2}}&{\textit{3}}\\% 

62 \hline% 

63 a&b&c\\% 

64 d&e&f\\% 

65 \hline% 

66 \end{tabular}% 

67 ' 

68 """ 

69 

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

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

72 stream.write(f"\\begin{{tabular}}{{{cols}}}%\n") 

73 

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

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

76 stream.write("\\end{tabular}%\n") 

77 

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

79 """Begin the header of a LaTeX table.""" 

80 stream.write("\\hline%\n") 

81 

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

83 """End the header of a LaTeX table.""" 

84 stream.write("\\hline%\n") 

85 

86 def end_table_section(self, stream: TextIOBase, cols: str, 

87 section_index: int, n_rows: int) -> None: 

88 """End a table section.""" 

89 stream.write("\\hline%\n") 

90 

91 def end_table_section_header(self, stream: TextIOBase, cols: str, 

92 section_index: int) -> None: 

93 """End a table section header.""" 

94 stream.write("\\hline%\n") 

95 

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

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

98 """End a row in a LaTeX table.""" 

99 stream.write("\\\\%\n") 

100 

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

102 section_index: int, row_index: int, 

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

104 """Begin a LaTeX table cell.""" 

105 if col_index > 0: 

106 stream.write("&") 

107 

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

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

110 """Print a text string.""" 

111 if len(text) <= 0: 

112 return 

113 if bold: 

114 stream.write("{\\textbf{") 

115 if italic: 

116 stream.write("{\\textit{") 

117 if code: 

118 stream.write("{\\texttt{") 

119 

120 if mode == TEXT: 

121 stream.write(text.replace("_", "\\_")) 

122 elif mode == NUMBER: 

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

124 if i < 0: 

125 i = text.find("E") 

126 if i > 0: 

127 stream.write(f"{text[:i]}{_EPREFIX}{text[i + 1:]}}}") 

128 else: 

129 stream.write(text.replace("_", "\\_")) 

130 elif mode == NAN: 

131 stream.write(r"$\emptyset$") 

132 elif mode == POSITIVE_INFINITY: 

133 stream.write(r"$\infty$") 

134 elif mode == NEGATIVE_INFINITY: 

135 stream.write(r"$-\infty") 

136 elif mode == SPECIAL: 

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

138 if s not in SPECIAL_CHARS: 

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

140 stream.write(SPECIAL_CHARS[s]) 

141 else: 

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

143 

144 if code: 

145 stream.write("}}") 

146 if italic: 

147 stream.write("}}") 

148 if bold: 

149 stream.write("}}") 

150 

151 def __str__(self): 

152 """ 

153 Get the appropriate file suffix. 

154 

155 :returns: the file suffix 

156 :retval 'tex': always 

157 """ 

158 return "tex" 

159 

160 @staticmethod 

161 def instance() -> "LaTeX": 

162 """ 

163 Get the LaTeX format singleton instance. 

164 

165 :returns: the singleton instance of the LaTeX format 

166 """ 

167 attr: Final[str] = "_instance" 

168 func: Final = LaTeX.instance 

169 if not hasattr(func, attr): 

170 setattr(func, attr, LaTeX()) 

171 return getattr(func, attr)