Coverage for moptipy / utils / markdown.py: 89%

70 statements  

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

1"""The markdown 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.latex import SPECIAL_CHARS as __SC 

15from moptipy.utils.text_format import MODE_TABLE_HEADER, TextFormatDriver 

16 

17#: the special chars 

18SPECIAL_CHARS: Final[dict[str, str]] = dict(__SC) 

19SPECIAL_CHARS["\u2014"] = "—" 

20 

21 

22class Markdown(TextFormatDriver): 

23 r""" 

24 The markdown text driver. 

25 

26 >>> from io import StringIO 

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

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

29 >>> s = StringIO() 

30 >>> md = Markdown.instance() 

31 >>> print(str(md)) 

32 md 

33 >>> with Table(s, "lrc", md) as t: 

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

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

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

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

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

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

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

41 ... r.cell("a") 

42 ... r.cell("b") 

43 ... r.cell("c") 

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

45 ... r.cell("d") 

46 ... r.cell("e") 

47 ... r.cell("f") 

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

49 '|**1**|`2`|*3*| 

50 |:--|--:|:-:| 

51 |a|b|c| 

52 |d|e|f| 

53 ' 

54 """ 

55 

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

57 """End the header of a markdown table.""" 

58 for c in cols: 

59 if c == "l": 

60 stream.write("|:--") 

61 elif c == "c": 

62 stream.write("|:-:") 

63 elif c == "r": 

64 stream.write("|--:") 

65 else: 

66 raise ValueError(f"Invalid col {c!r} in {cols!r}.") 

67 stream.write("|\n") 

68 

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

70 section_index: int, row_index: int, 

71 row_mode: int) -> None: 

72 """Begin a row in a markdown table.""" 

73 if (row_mode == MODE_TABLE_HEADER) and (row_index != 0): 

74 raise ValueError("pandoc markdown only supports one single header" 

75 f" row, but encountered row {row_mode + 1}.") 

76 

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

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

79 """End a row in a Markdown table.""" 

80 stream.write("|\n") 

81 

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

83 section_index: int, row_index: int, 

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

85 """Begin a Markdown table cell.""" 

86 stream.write("|") 

87 

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

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

90 """Print a text string.""" 

91 if len(text) <= 0: 

92 return 

93 if bold: 

94 stream.write("**") 

95 if italic: 

96 stream.write("*") 

97 if code: 

98 stream.write("`") 

99 

100 if mode == TEXT: 

101 stream.write(text) 

102 elif mode == NUMBER: 

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

104 if i < 0: 

105 i = text.find("E") 

106 if i > 0: 

107 stream.write(f"{text[:i]}\\*10^{text[i + 1:]}^") # \u00D7 

108 else: 

109 stream.write(text) 

110 elif mode == NAN: 

111 stream.write(r"$\emptyset$") # \u2205 

112 elif mode == POSITIVE_INFINITY: 

113 stream.write(r"$\infty$") # \u221E 

114 elif mode == NEGATIVE_INFINITY: 

115 stream.write(r"$-\infty$") # -\u221E 

116 elif mode == SPECIAL: 

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

118 if s not in SPECIAL_CHARS: 

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

120 stream.write(SPECIAL_CHARS[s]) 

121 else: 

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

123 

124 if code: 

125 stream.write("`") 

126 if italic: 

127 stream.write("*") 

128 if bold: 

129 stream.write("**") 

130 

131 def __str__(self): 

132 """ 

133 Get the appropriate file suffix. 

134 

135 :returns: the file suffix 

136 :retval 'md': always 

137 """ 

138 return "md" 

139 

140 @staticmethod 

141 def instance() -> "Markdown": 

142 """ 

143 Get the markdown format singleton instance. 

144 

145 :returns: the singleton instance of the Markdown format 

146 """ 

147 attr: Final[str] = "_instance" 

148 func: Final = Markdown.instance 

149 if not hasattr(func, attr): 

150 setattr(func, attr, Markdown()) 

151 return getattr(func, attr)