Coverage for liitos / captions.py: 97.50%

58 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-03 22:54:48 +00:00

1"""Move a caption below a table. 

2 

3Implementer note: We use a three state machine with transitions COPY [-> TABLE [-> CAPTION -> TABLE] -> COPY]. 

4""" 

5 

6from collections.abc import Iterable 

7from enum import Enum 

8 

9from liitos import log 

10 

11Caption = list[str] 

12Table = list[str] 

13 

14Modus = Enum('Modus', 'COPY TABLE CAPTION') 

15 

16TABLE_START_TRIGGER_STARTSWITH = r'\begin{longtable}' 

17CAPTION_START_TRIGGER_STARTSWITH = r'\caption{' 

18CAPTION_END_TRIGGER_ENDSWITH = r'}\tabularnewline' 

19TABLE_END_TRIGGER_STARTSWITH = r'\end{longtable}' 

20 

21 

22def filter_seek_table(line: str, slot: int, modus: Modus, outgoing: list[str], table: Table, caption: Caption) -> Modus: 

23 r"""Filter line, seek for a table, if found init table and caption, and return updated mnodus. 

24 

25 Examples: 

26 

27 >>> o = [] 

28 >>> line = TABLE_START_TRIGGER_STARTSWITH # r'\begin{longtable}' 

29 >>> t, c = [], [] 

30 >>> m = filter_seek_table(line, 0, Modus.COPY, o, t, c) 

31 >>> assert not o 

32 >>> m.name 

33 'TABLE' 

34 >>> t 

35 ['\\begin{longtable}'] 

36 >>> assert c == [] 

37 """ 

38 if line.startswith(TABLE_START_TRIGGER_STARTSWITH): 

39 log.info(f'start of a table environment at line #{slot + 1}') 

40 modus = Modus.TABLE 

41 table.append(line) 

42 caption.clear() 

43 else: 

44 outgoing.append(line) 

45 

46 return modus 

47 

48 

49def filter_seek_caption( 

50 line: str, slot: int, modus: Modus, outgoing: list[str], table: Table, caption: Caption 

51) -> Modus: 

52 r"""Filter line in table, seek for a caption, and return updated mnodus. 

53 

54 Examples: 

55 

56 >>> o = [] 

57 >>> line = CAPTION_START_TRIGGER_STARTSWITH # r'\caption{' 

58 >>> t, c = [], [] 

59 >>> m = filter_seek_caption(line, 0, Modus.TABLE, o, t, c) 

60 >>> assert not o 

61 >>> m.name 

62 'CAPTION' 

63 >>> t 

64 [] 

65 >>> c 

66 ['\\caption{'] 

67 

68 >>> o = [] 

69 >>> line = r'\caption{something maybe}\tabularnewline' 

70 >>> t, c = ['foo'], ['bar'] 

71 >>> m = filter_seek_caption(line, 0, Modus.TABLE, o, t, c) 

72 >>> o 

73 [] 

74 >>> m.name 

75 'TABLE' 

76 >>> t 

77 ['foo'] 

78 >>> c 

79 ['bar', '\\caption{something maybe}\\tabularnewline'] 

80 

81 >>> o = [] 

82 >>> line = r'\end{longtable}' 

83 >>> t, c = ['foo', r'\endlastfoot'], ['bar'] 

84 >>> m = filter_seek_caption(line, 0, Modus.TABLE, o, t, c) 

85 >>> o 

86 ['foo', 'bar', '\\endlastfoot', '\\end{longtable}'] 

87 >>> m.name 

88 'COPY' 

89 >>> assert t == [] 

90 >>> assert c == [] 

91 """ 

92 if line.startswith(CAPTION_START_TRIGGER_STARTSWITH): 

93 log.info(f'- found the caption start at line #{slot + 1}') 

94 caption.append(line) 

95 if not line.strip().endswith(CAPTION_END_TRIGGER_ENDSWITH): 95 ↛ 115line 95 didn't jump to line 115 because the condition on line 95 was always true

96 log.info(f'- multi line caption at line #{slot + 1}') 

97 modus = Modus.CAPTION 

98 elif line.startswith(TABLE_END_TRIGGER_STARTSWITH): 

99 log.info(f'end of table env detected at line #{slot + 1}') 

100 while table: 

101 stmt = table.pop(0) 

102 if not stmt.startswith(r'\endlastfoot'): 

103 outgoing.append(stmt) 

104 continue 

105 else: 

106 while caption: 

107 outgoing.append(caption.pop(0)) 

108 outgoing.append(stmt) 

109 outgoing.append(line) 

110 modus = Modus.COPY 

111 else: 

112 log.debug('- table continues') 

113 table.append(line) 

114 

115 return modus 

116 

117 

118def filter_collect_caption( 

119 line: str, slot: int, modus: Modus, outgoing: list[str], table: Table, caption: Caption 

120) -> Modus: 

121 r"""Filter line in caption until end marker, and return updated mnodus. 

122 

123 Examples: 

124 

125 >>> o = [] 

126 >>> line = 'some caption text' 

127 >>> t, c = ['foo'], ['bar'] 

128 >>> m = filter_collect_caption(line, 0, Modus.CAPTION, o, t, c) 

129 >>> assert not o 

130 >>> m.name 

131 'CAPTION' 

132 >>> assert t == ['foo'] 

133 >>> assert c == ['bar', 'some caption text'] 

134 

135 >>> o = [] 

136 >>> line = r'}\tabularnewline' 

137 >>> t, c = ['foo'], ['bar'] 

138 >>> m = filter_collect_caption(line, 0, Modus.CAPTION, o, t, c) 

139 >>> assert not o 

140 >>> m.name 

141 'TABLE' 

142 >>> assert t == ['foo'] 

143 >>> assert c == ['bar', r'}\tabularnewline'] 

144 """ 

145 caption.append(line) 

146 if line.strip().endswith(CAPTION_END_TRIGGER_ENDSWITH): 146 ↛ 150line 146 didn't jump to line 150 because the condition on line 146 was always true

147 log.info(f'- caption read at line #{slot + 1}') 

148 modus = Modus.TABLE 

149 

150 return modus 

151 

152 

153def weave(incoming: Iterable[str], lookup: dict[str, str] | None = None) -> list[str]: 

154 r"""Weave the table caption inside foot from (default) head of table. 

155 

156 Examples: 

157 

158 >>> incoming = [''] 

159 >>> o = weave(incoming) 

160 >>> o 

161 [''] 

162 

163 >>> i = [ 

164 ... r'\begin{longtable}[]{@{}|l|c|r|@{}}', 

165 ... r'\caption{The old tune\label{tab:tuna}}\tabularnewline', 

166 ... r'\hline\noalign{}\rowcolor{light-gray}', 

167 ... r'Foo & Bar & Baz \\', 

168 ... r'\hline\noalign{}', 

169 ... r'\endfirsthead', 

170 ... r'\hline\noalign{}\rowcolor{light-gray}', 

171 ... r'Foo & Bar & Baz \\', 

172 ... r'\hline\noalign{}', 

173 ... r'\endhead', 

174 ... r'\hline\noalign{}', 

175 ... r'\endlastfoot', 

176 ... r'Quux & x & 42 \\ \hline', 

177 ... r'xQuu & y & -1 \\ \hline', 

178 ... r'uxQu & z & true \\', 

179 ... r'\end{longtable}', 

180 ... ] 

181 >>> o = weave(i) 

182 >>> o 

183 ['\\begin{longtable}[]{@{}|l|c|r|@{}}', ... '\\caption{The old tune..., '\\endlastfoot', ...] 

184 """ 

185 outgoing: list[str] = [] 

186 modus = Modus.COPY 

187 table: Table = [] 

188 caption: Caption = [] 

189 for slot, line in enumerate(incoming): 

190 if modus == Modus.COPY: 

191 modus = filter_seek_table(line, slot, modus, outgoing, table, caption) 

192 elif modus == Modus.TABLE: 

193 modus = filter_seek_caption(line, slot, modus, outgoing, table, caption) 

194 else: # modus == Modus.CAPTION: 

195 modus = filter_collect_caption(line, slot, modus, outgoing, table, caption) 

196 

197 return outgoing