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
« 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.
3Implementer note: We use a three state machine with transitions COPY [-> TABLE [-> CAPTION -> TABLE] -> COPY].
4"""
6from collections.abc import Iterable
7from enum import Enum
9from liitos import log
11Caption = list[str]
12Table = list[str]
14Modus = Enum('Modus', 'COPY TABLE CAPTION')
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}'
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.
25 Examples:
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)
46 return modus
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.
54 Examples:
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{']
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']
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)
115 return modus
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.
123 Examples:
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']
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
150 return modus
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.
156 Examples:
158 >>> incoming = ['']
159 >>> o = weave(incoming)
160 >>> o
161 ['']
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)
197 return outgoing