Coverage for liitos/tables.py: 79.91%
453 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-05 17:22:35 +00:00
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-05 17:22:35 +00:00
1"""Apply all pairs in patch module on document."""
3import re
4from collections.abc import Iterable, Iterator
5from typing import Union, no_type_check
7from liitos import log
9# The "target pattern for a line base minimal regex parser"
10_ = r"""
11\columns=
13\begin{longtable}[]{
14... \real{fwidth_1}}
15...
16... \real{fwidth_n}}@{}}
17\toprule\noalign{}
18\begin{minipage}[
19text_1
20\end{minipage} & \begin{minipage}[
21text_2
22...
23\end{minipage} & \begin{minipage}[
24text_n
25\end{minipage} \\
26\midrule\noalign{}
27\endfirsthead
28\toprule\noalign{}
29\begin{minipage}[
30text_1
31\end{minipage} & \begin{minipage}[
32text_2
33...
34\end{minipage} & \begin{minipage}[
35text_n
36\end{minipage} \\
37\midrule\noalign{}
38\endhead
39\bottomrule\noalign{}
40\endlastfoot
41cell_1_1 & cell_1_2 & ... & cell_1_n \\
42cell_1_2 & cell_2_2 & ... & cell_2_n \\
43...
44row_1_m & row_2_m & ... & cell_n_m \\
45\rowcolor{white}
46\caption{cap_text_x
47cap_text_y\tabularnewline
48\end{longtable}
49"""
51TAB_START_TOK = r'\begin{longtable}[]{' # '@{}'
52TOP_RULE = r'\toprule()'
53MID_RULE = r'\midrule()'
54END_HEAD = r'\endhead'
55END_DATA_ROW = r'\\'
56BOT_RULE = r'\bottomrule()'
57TAB_END_TOK = r'\end{longtable}'
59TAB_NEW_START = r"""\begin{small}
60\begin{longtable}[]{|
61>{\raggedright\arraybackslash}p{(\columnwidth - 12\tabcolsep) * \real{0.1500}}|
62>{\raggedright\arraybackslash}p{(\columnwidth - 12\tabcolsep) * \real{0.5500}}|
63>{\raggedright\arraybackslash}p{(\columnwidth - 12\tabcolsep) * \real{0.1500}}|
64>{\raggedright\arraybackslash}p{(\columnwidth - 12\tabcolsep) * \real{0.2000}}|}
65\hline"""
67TAB_HACKED_HEAD = r"""\begin{minipage}[b]{\linewidth}\raggedright
68\ \mbox{\textbf{Key}}
69\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
70\mbox{\textbf{Summary}}
71\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
72\mbox{\textbf{Parent}} \mbox{\textbf{Requirement}}
73\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
74\mbox{\textbf{Means of}} \mbox{\textbf{Compliance (MOC)}}
75\end{minipage} \\
76\hline
77\endfirsthead
78\multicolumn{4}{@{}l}{\small \ldots continued}\\\hline
79\hline
80\begin{minipage}[b]{\linewidth}\raggedright
81\ \mbox{\textbf{Key}}
82\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
83\mbox{\textbf{Summary}}
84\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
85\mbox{\textbf{Parent}} \mbox{\textbf{Requirement}}
86\end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
87\mbox{\textbf{Means of}} \mbox{\textbf{Compliance (MOC)}}
88\end{minipage} \\
89\endhead
90\hline"""
92NEW_RULE = r'\hline'
94TAB_NEW_END = r"""\end{longtable}
95\end{small}
96\vspace*{-2em}
97\begin{footnotesize}
98ANNOTATION
99\end{footnotesize}"""
101COMMA = ','
104class Table:
105 """Some adhoc structure to encapsulate the source and target table."""
107 SourceMapType = list[tuple[int, str]]
108 ColumnsType = dict[str, dict[str, Union[float, int, str]]]
110 # ---- begin of LBP skeleton / shape ---
111 LBP_STARTSWITH_TAB_ENV_BEGIN = r'\begin{longtable}[]{'
112 LBP_REAL_INNER_COLW_PAT = re.compile(r'^(?P<clspec>.+)\\real{(?P<cwval>[0-9.]+)}}\s*$')
113 LBP_REAL_OUTER_COLW_PAT = re.compile(r'^(?P<clspec>.+)\\real{(?P<cwval>[0-9.]+)}}@{}}\s*$')
114 # Width lines for header:
115 FUT_LSPLIT_ONCE_FOR_PREFIX_VAL_COMMA_RIGHT = '}}'
116 FUT_LSPLIT_ONCE_FOR_PREFIX_COMMA_VAL = r'\real{'
117 # then concat PREFIX + r'\real{' + str(column_width_new) + '}}' + RIGHT
119 LBP_TOP_RULE_CONTEXT_STARTSWITH = r'\toprule\noalign{}'
120 LPB_START_COLUMN_LABEL_STARTSWITH = r'\begin{minipage}['
121 LBP_SEP_COLUMN_LABEL_STARTSWITH = r'\end{minipage} & \begin{minipage}['
122 LBP_STOP_COLUMN_LABEL_STARTSWITH = r'\end{minipage} \\'
124 LBP_MID_RULE_CONTEXT_STARTSWITH = r'\midrule\noalign{}'
126 LBP_END_FIRST_HEAD_STARTSWITH = r'\endfirsthead'
128 # LBP_TOP_RULE_CONTEXT_STARTSWITH = r'\toprule\noalign{}'
129 # LPB_START_COLUMN_LABEL_STARTSWITH = r'\begin{minipage}['
130 # LBP_SEP_COLUMN_LABEL_STARTSWITH = r'\end{minipage} & \begin{minipage}['
131 # LBP_STOP_COLUMN_LABEL_STARTSWITH = r'\end{minipage} \\'
133 # LBP_MID_RULE_CONTEXT_STARTSWITH = r'\midrule\noalign{}'
135 LBP_END_ALL_HEAD_STARTSWITH = r'\endhead'
137 LBP_BOTTOM_RULE_CONTEXT_STARTSWITH = r'\bottomrule\noalign{}'
139 LBP_END_LAST_FOOT_STARTSWITH = r'\endlastfoot'
141 # ... data lines - we want inject of r'\hline' following every data line (not text line)
142 # -> that is, inject after lines ending with r'\\'
144 LBP_END_OF_DATA_STARTSWITH = r'\rowcolor{white}'
145 LBP_START_CAP_STARTSWITH = r'\caption{'
146 LBP_STOP_CAP_ENDSWITH = r'\tabularnewline'
147 LBP_STARTSWITH_TAB_ENV_END = r'\end{longtable}'
149 # ---- end of LBP skeleton / shape ---
150 @no_type_check
151 def __init__(
152 self,
153 anchor: int,
154 start_line: str,
155 text_lines: Iterator[str],
156 widths: list[float],
157 font_sz: str = '',
158 style: str = 'readable',
159 ):
160 """Initialize the table from source text lines anchored at anchor.
161 The implementation allows reuse of the iterator on caller site for extracting subsequent tables in one go.
162 """
163 self.src_map: Table.SourceMapType = [(anchor, start_line.rstrip())]
164 self.data_row_ends: Table.SourceMapType = []
165 self.columns: Table.ColumnsType = {}
166 self.target_widths: list[float] = widths
167 self.source_widths: list[float] = []
168 self.font_size = font_sz
169 self.style: str = style if style in ('readable', 'ugly') else 'readable'
170 log.info(f'Received {anchor=}, {start_line=}, target {widths=}, {font_sz=}, and {style=}')
171 local_number = 0
172 consumed = False
173 while not consumed:
174 local_number += 1
175 pos = local_number + anchor
176 line = next(text_lines).rstrip() # May raise StopIteration, deal with it (later)
177 self.src_map.append((pos, line))
178 if line.startswith(Table.LBP_STARTSWITH_TAB_ENV_END):
179 consumed = True
181 self.parse_columns()
182 self.parse_data_rows()
183 self.data_row_count = len(self.data_row_ends)
184 self.cw_patches: dict[str, str] = {}
185 self.create_width_patches()
186 log.info(f'Parsed {len(self.target_widths)} x {self.data_row_count} table starting at anchor {anchor}')
188 @no_type_check
189 def create_width_patches(self):
190 """If widths are meaningful and consistent create the patches with the zero-based line-numbers as keys."""
191 if not self.source_widths:
192 log.warning('Found no useful width information')
193 return {}
194 wrapper = r'\real{'
195 postfix = '}}'
196 finalize = '@{}}'
197 if self.style == 'ugly':
198 finalize = '|@{}}'
199 ranks = list(self.columns)
200 for rank in ranks:
201 anchor_str = str(self.columns[rank]['col_spec_line'])
202 prefix = self.columns[rank]['colspec_prefix']
203 value = self.columns[rank]['width']
204 if self.style == 'ugly' and prefix.lstrip().startswith('>{'):
205 prefix = prefix.replace('>{', '|>{', 1)
206 # concat PREFIX + r'\real{' + str(column_width_new) + '}}'
207 self.cw_patches[anchor_str] = prefix + wrapper + str(value) + postfix
208 if rank == ranks[-1]:
209 self.cw_patches[anchor_str] += finalize
211 def width_patches(self) -> dict[str, str]:
212 """Return the map of width patches with the zero-based line-numbers as keys."""
213 return self.cw_patches
215 def source_map(self) -> SourceMapType:
216 """Return the source map data (a random accessible sequence of pairs) mapping abs line number to text line."""
217 return self.src_map
219 def column_data(self) -> ColumnsType:
220 """Return the column data (an ordered dict of first labels, other labels, and widths) with abs line map."""
221 return self.columns
223 def column_source_widths(self) -> list[float]:
224 """Return the incoming column widths."""
225 return self.source_widths
227 def column_target_widths(self) -> list[float]:
228 """Return the outgoing column widths."""
229 return self.target_widths
231 @no_type_check
232 def table_width(self) -> float:
233 """Return the sum of all column widths."""
234 return sum(self.columns[r].get('width', 0) for r in self.columns)
236 def data_row_seps(self) -> SourceMapType:
237 """Return the map to the data row ends for injecting separators."""
238 return self.data_row_ends
240 @no_type_check
241 def transform_widths(self) -> None:
242 """Apply the target transform to column widths."""
243 self.source_widths = [self.columns[rank]['width'] for rank in self.columns]
244 if not self.target_widths:
245 log.info('No target widths given - maintaining source column widths')
246 self.target_widths = self.source_widths
247 return
248 if len(self.target_widths) != len(self.source_widths):
249 if len(self.source_widths): 249 ↛ 250line 249 didn't jump to line 250 because the condition on line 249 was never true
250 log.warning(
251 f'Mismatching {len(self.target_widths)} target widths given - maintaining'
252 f' the {len(self.source_widths)} source column widths'
253 )
254 self.target_widths = self.source_widths
255 return
256 else:
257 log.warning(
258 f'Lacking implementation for {len(self.target_widths)} target widths given - maintaining'
259 f' the {len(self.source_widths)} source column widths'
260 )
261 self.target_widths = self.source_widths
262 return
264 log.info('Applying target widths given - adapting source column widths')
265 for rank, target_width in zip(self.columns, self.target_widths):
266 self.columns[rank]['width'] = target_width
268 @no_type_check
269 def parse_columns(self) -> None:
270 """Parse the head to extract the columns."""
271 self.parse_column_widths()
272 self.parse_column_first_head()
273 self.parse_column_other_head()
274 self.transform_widths()
276 def parse_column_widths(self) -> None:
277 r"""Parse the column width declarations to initialize the columns data.
279 \begin{longtable}[]{@{}%wun-based-line-9
280 >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.1118}}
281 >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.5776}}
282 >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.1739}}
283 >{\raggedright\arraybackslash}p{(\columnwidth - 6\tabcolsep) * \real{0.1366}}@{}}
284 \toprule\noalign{}
285 """
286 rank = 0
287 for anchor, text in self.src_map:
288 if text.startswith(Table.LBP_STARTSWITH_TAB_ENV_BEGIN):
289 continue
290 if text.startswith(Table.LBP_TOP_RULE_CONTEXT_STARTSWITH):
291 break
292 m = Table.LBP_REAL_INNER_COLW_PAT.match(text)
293 if m:
294 self.columns[str(rank)] = {
295 'first_label': '',
296 'first_label_line': -1,
297 'continued_label': '',
298 'continued_label_line': -1,
299 'col_spec_line': anchor,
300 'colspec_prefix': m.groupdict()['clspec'],
301 'width': float(m.groupdict()['cwval']),
302 }
303 rank += 1
304 continue
305 m = Table.LBP_REAL_OUTER_COLW_PAT.match(text)
306 if m:
307 self.columns[str(rank)] = {
308 'first_label': '',
309 'first_label_line': -1,
310 'continued_label': '',
311 'continued_label_line': -1,
312 'col_spec_line': anchor,
313 'colspec_prefix': m.groupdict()['clspec'],
314 'width': float(m.groupdict()['cwval']),
315 }
316 rank += 1
317 continue
319 def parse_column_first_head(self) -> None:
320 r"""Parse the head to extract the columns.
322 \begin{minipage}[b]{\linewidth}\raggedright
323 Parameter
324 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
325 Description
326 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
327 Name
328 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
329 Example
330 \end{minipage} \\
331 \midrule\noalign{}
332 \endfirsthead
333 """
334 rank = 0
335 first_head = False
336 label_next = False
337 for anchor, text in self.src_map:
338 if text.startswith(Table.LBP_TOP_RULE_CONTEXT_STARTSWITH):
339 first_head = True
340 continue
341 if not first_head:
342 continue
343 if text.startswith(Table.LBP_END_FIRST_HEAD_STARTSWITH):
344 break
345 if text.startswith(Table.LPB_START_COLUMN_LABEL_STARTSWITH):
346 label_next = True
347 continue
348 if text.startswith(Table.LBP_SEP_COLUMN_LABEL_STARTSWITH):
349 label_next = True
350 continue
351 if text.startswith(Table.LBP_STOP_COLUMN_LABEL_STARTSWITH): 351 ↛ 352line 351 didn't jump to line 352 because the condition on line 351 was never true
352 label_next = True
353 continue
354 if label_next:
355 self.columns[str(rank)]['first_label'] = text.strip()
356 self.columns[str(rank)]['first_label_line'] = anchor
357 rank += 1
358 if str(rank) in self.columns:
359 continue
360 break
362 def parse_column_other_head(self) -> None:
363 r"""Parse the other heads to extract the column labelss.
365 \endfirsthead
366 \toprule\noalign{}
367 \begin{minipage}[b]{\linewidth}\raggedright
368 Parameter
369 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
370 Description
371 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
372 Name
373 \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright
374 Example
375 \end{minipage} \\
376 \midrule\noalign{}
377 \endhead
378 """
379 rank = 0
380 continued_head = False
381 label_next = False
382 for anchor, text in self.src_map:
383 if text.startswith(Table.LBP_END_FIRST_HEAD_STARTSWITH):
384 continued_head = True
385 continue
386 if not continued_head:
387 continue
388 if text.startswith(Table.LBP_END_ALL_HEAD_STARTSWITH):
389 break
390 if text.startswith(Table.LPB_START_COLUMN_LABEL_STARTSWITH):
391 label_next = True
392 continue
393 if text.startswith(Table.LBP_SEP_COLUMN_LABEL_STARTSWITH):
394 label_next = True
395 continue
396 if text.startswith(Table.LBP_STOP_COLUMN_LABEL_STARTSWITH): 396 ↛ 397line 396 didn't jump to line 397 because the condition on line 396 was never true
397 label_next = True
398 continue
399 if label_next:
400 self.columns[str(rank)]['continued_label'] = text.strip()
401 self.columns[str(rank)]['continued_label_line'] = anchor
402 rank += 1
403 if str(rank) in self.columns:
404 continue
405 break
407 def parse_data_rows(self) -> None:
408 r"""Parse the data rows.
410 \endlastfoot
411 A2 & B2 & C2 & D2 \\
412 \end{longtable}
413 """
414 data_section = False
415 for anchor, text in self.src_map: 415 ↛ exitline 415 didn't return from function 'parse_data_rows' because the loop on line 415 didn't complete
416 if text.startswith(Table.LBP_END_LAST_FOOT_STARTSWITH):
417 data_section = True
418 continue
419 if text.startswith(Table.LBP_STARTSWITH_TAB_ENV_END):
420 break
421 if data_section and r'\\' in text:
422 if self.style == 'ugly':
423 text += r' \hline'
424 self.data_row_ends.append((anchor, text))
425 continue
428def parse_table_font_size_command(slot: int, text_line: str) -> tuple[bool, str, str]:
429 """Parse the \\tablefontsize=footnotesize command."""
430 backslash = '\\'
431 known_sizes = (
432 'tiny',
433 'scriptsize',
434 'footnotesize',
435 'small',
436 'normalsize',
437 'large',
438 'Large',
439 'LARGE',
440 'huge',
441 'Huge',
442 )
443 if text_line.startswith(r'\tablefontsize='):
444 log.info(f'trigger a fontsize mod for the next table environment at line #{slot + 1}|{text_line}')
445 try:
446 font_size = text_line.split('=', 1)[1].strip() # r'\tablefontsize=Huge' --> 'Huge'
447 if font_size.startswith(backslash):
448 font_size = font_size.lstrip(backslash)
449 if font_size not in known_sizes:
450 log.error(f'failed to map given fontsize ({font_size}) into known sizes ({",".join(known_sizes)})')
451 return False, text_line, ''
452 log.info(f' -> parsed table fontsize mod as ({font_size})')
453 return True, '', font_size
454 except Exception as err:
455 log.error(f'failed to parse table fontsize value from {text_line.strip()} with err: {err}')
456 return False, text_line, ''
457 else:
458 return False, text_line, ''
461def parse_columns_command(slot: int, text_line: str) -> tuple[bool, str, list[float]]:
462 r"""Parse the \\columns=,0.2,0.7 command.
464 Examples:
466 >>> slot = 0
467 >>> text = '\\columns=,0.2,0.7'
468 >>> parse_columns_command(slot, text)
469 (True, '', [0.1, 0.2, 0.7])
471 >>> slot = 0
472 >>> text = '\\columns=0.1,0.2,0.7'
473 >>> parse_columns_command(slot, text)
474 (True, '', [0.1, 0.2, 0.7])
476 >>> slot = 0
477 >>> text = '\\columns=0.4,0.7'
478 >>> parse_columns_command(slot, text)
479 (True, '', [0.4, 0.7])
481 >>> slot = 0
482 >>> text = r'\columns= , 20\%,70\%'
483 >>> parse_columns_command(slot, text)
484 (True, '', [0.1, 0.2, 0.7])
486 >>> slot = 0
487 >>> text = '\\columns===='
488 >>> parse_columns_command(slot, text)
489 (False, '\\columns====', [])
491 >>> slot = 0
492 >>> text = '\\columns=a,42,-1'
493 >>> parse_columns_command(slot, text)
494 (False, '\\columns=a,42,-1', [])
495 """
496 if text_line.startswith(r'\columns='): 496 ↛ 510line 496 didn't jump to line 510 because the condition on line 496 was always true
497 log.info(f'trigger a columns mod for the next table environment at line #{slot + 1}|{text_line}')
498 try:
499 cols_csv = text_line.split('=', 1)[1].strip() # r'\columns= , 20\%,70\%' --> r', 20\%,70\%'
500 cols = [v.strip() for v in cols_csv.split(COMMA)]
501 widths = [float(v.replace(r'\%', '')) / 100 if r'\%' in v else (float(v) if v else 0) for v in cols]
502 rest = round(1 - sum(round(w, 5) for w in widths), 5)
503 widths = [v if v else rest for v in widths]
504 log.info(f' -> parsed columns mod as | {" | ".join(str(round(v, 2)) for v in widths)} |')
505 return True, '', widths
506 except Exception as err:
507 log.error(f'failed to parse columns values from {text_line.strip()} with err: {err}')
508 return False, text_line, []
509 else:
510 return False, text_line, []
513@no_type_check
514def patch(incoming: Iterable[str], lookup: Union[dict[str, str], None] = None) -> list[str]:
515 r"""Later alligator. \\columns=,0.2,0.7 as mandatory trigger
517 Examples:
519 >>> incoming = ['foo', '', '\\columns=,0.5', '', TAB_START_TOK, '', TAB_END_TOK, 'bar', 'baz', 'quux']
520 >>> patch(incoming)
521 ['foo', '', '%CONSIDERED_\\columns=,0.5', '', '\\begin{longtable}[]{', '', '\\end{longtable}', 'bar', 'baz', 'quux']
523 >>> incoming = [
524 ... r'\begin{longtable}[]{@{}lcr@{}}',
525 ... r'\toprule\noalign{}',
526 ... r'Foo & Bar & Baz \\',
527 ... r'\midrule\noalign{}',
528 ... r'\endfirsthead',
529 ... r'\toprule\noalign{}',
530 ... r'Foo & Bar & Baz \\',
531 ... r'\midrule\noalign{}',
532 ... r'\endhead',
533 ... r'\bottomrule\noalign{}',
534 ... r'\caption{The old tune\label{tab:tuna}}\tabularnewline',
535 ... r'\endlastfoot',
536 ... r'Quux & x & 42 \\',
537 ... r'\end{longtable}',
538 ... ]
539 >>> patch(incoming)
540 ['\\begin{longtable}[]{@{}lcr@{}}', '\\toprule\\noalign{}', 'Foo & Bar & Baz \\\\', ...'\\end{longtable}']
541 """
542 table_style = 'readable' if lookup is None else lookup.get('table_style', 'readable')
543 log.info(f'requested table style is ({table_style})')
544 table_section, head, annotation = False, False, False
545 table_ranges = []
546 guess_slot = 0
547 table_range = {}
548 has_column = False
549 has_font_size = False
550 widths: list[float] = []
551 font_size = ''
552 comment_outs = []
553 for n, text in enumerate(incoming):
554 if not table_section: 554 ↛ 577line 554 didn't jump to line 577 because the condition on line 554 was always true
555 if not has_font_size: 555 ↛ 559line 555 didn't jump to line 559 because the condition on line 555 was always true
556 has_font_size, text_line, font_size = parse_table_font_size_command(n, text)
557 if has_font_size: 557 ↛ 558line 557 didn't jump to line 558 because the condition on line 557 was never true
558 comment_outs.append(n)
559 if not has_font_size: 559 ↛ 562line 559 didn't jump to line 562 because the condition on line 559 was always true
560 continue
562 if not has_column:
563 has_column, text_line, widths = parse_columns_command(n, text)
564 if has_column:
565 comment_outs.append(n)
566 if not has_column:
567 continue
569 if not text.startswith(TAB_START_TOK):
570 continue
571 table_range['start'] = n
572 table_section = True
573 head = True
574 table_range['end_data_row'] = []
575 continue
577 if text.startswith(TOP_RULE):
578 table_range['top_rule'] = n
579 continue
581 if text.startswith(MID_RULE):
582 table_range['mid_rule'] = n
583 continue
585 if text.startswith(END_HEAD):
586 table_range['end_head'] = n
587 head = False
588 continue
590 if not head and text.strip().endswith(END_DATA_ROW):
591 table_range['end_data_row'].append(n)
592 continue
594 if text.startswith(BOT_RULE):
595 table_range['bottom_rule'] = n
596 continue
598 if text.startswith(TAB_END_TOK):
599 table_range['end'] = n
600 annotation = True
601 guess_slot = n + 2
602 continue
604 if annotation and n == guess_slot:
605 table_range['amend'] = n
606 table_ranges.append(table_range)
607 table_range = {}
608 annotation, table_section = False, False
610 log.info(f'Detected {len(table_ranges)} tables (method from before version 2023.2.12):')
611 for thing in table_ranges: 611 ↛ 612line 611 didn't jump to line 612 because the loop on line 611 never started
612 log.info(f'- {thing}')
614 tables_in, on_off_slots = [], []
615 for table in table_ranges: 615 ↛ 616line 615 didn't jump to line 616 because the loop on line 615 never started
616 from_here = table['start']
617 thru_there = table['amend']
618 log.info('Table:')
619 log.info(f'-from {incoming[from_here]}')
620 log.info(f'-thru {incoming[thru_there]}')
621 on_off = (from_here, thru_there + 1)
622 on_off_slots.append(on_off)
623 tables_in.append((on_off, [line for line in incoming[on_off[0] : on_off[1]]]))
625 log.debug('# - - - 8< - - -')
626 if tables_in: 626 ↛ 627line 626 didn't jump to line 627 because the condition on line 626 was never true
627 log.debug(str('\n'.join(tables_in[0][1])))
628 log.debug('# - - - 8< - - -')
630 reader = iter(incoming)
631 tables = []
632 comment_outs = []
633 n = 0
634 widths = []
635 font_size = ''
636 for line in reader:
637 log.debug(f'zero-based-line-no={n}, text=({line}) table-count={len(tables)}')
638 if not line.startswith(Table.LBP_STARTSWITH_TAB_ENV_BEGIN):
639 if line.startswith(r'\tablefontsize='): 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true
640 has_font_size, text_line, font_size = parse_table_font_size_command(n, line)
641 log.info(f' + {has_font_size=}, {text_line=}, {font_size=}')
642 if has_font_size:
643 comment_outs.append(n)
644 log.info(f'FONT-SIZE at <<{n}>>')
645 if line.startswith(r'\columns='):
646 has_column, text_line, widths = parse_columns_command(n, line)
647 log.info(f' + {has_column=}, {text_line=}, {widths=}')
648 if has_column: 648 ↛ 651line 648 didn't jump to line 651 because the condition on line 648 was always true
649 comment_outs.append(n)
650 log.info(f'COLUMNS-WIDTH at <<{n}>>')
651 n += 1
652 else:
653 table = Table(n, line, reader, widths, font_size, style=table_style)
654 tables.append(table)
655 n += len(tables[-1].source_map())
656 log.debug(f'- incremented n to {n}')
657 log.debug(f'! next n (zero offset) is {n}')
658 widths = []
659 font_size = ''
661 log.info('---')
662 for n, table in enumerate(tables, start=1):
663 log.info(f'Table #{n} (total width = {table.table_width()}):')
664 for rank, column in table.column_data().items():
665 log.info(f'{rank} -> {column}')
666 log.info(f'- source widths = {table.column_source_widths()}):')
667 log.info(f'- target widths = {table.column_target_widths()}):')
668 for numba, replacement in table.width_patches().items():
669 log.info(f'{numba} -> {replacement}')
670 for anchor, text in table.data_row_seps():
671 log.info(f'[data-row-seps]:{anchor} -> {text}')
672 log.info(f'= (fontsize command = "{table.font_size}"):')
673 log.info('---')
674 log.info(f'Comment out the following {len(comment_outs)} lines (zero based numbers) - punch:')
675 for number in comment_outs:
676 log.info(f'- {number}')
678 wideners = {}
679 for table in tables:
680 for numba, replacement in table.width_patches().items():
681 wideners[numba] = replacement
682 widen_me = set(wideners)
683 log.debug('widen me has:')
684 log.debug(list(widen_me))
685 log.debug('--- global replacement width lines: ---')
686 for numba, replacement in wideners.items():
687 log.debug(f'{numba} => {replacement}')
688 log.debug('---')
690 sizers = {}
691 for table in tables:
692 if table.font_size: 692 ↛ 693line 692 didn't jump to line 693 because the condition on line 692 was never true
693 sizers[str(table.src_map[0][0])] = '\\begin{' + table.font_size + '}\n' + table.src_map[0][1]
694 sizers[str(table.src_map[-1][0])] = table.src_map[-1][1] + '\n' + '\\end{' + table.font_size + '}'
695 size_me = set(sizers)
696 log.debug('size me has:')
697 log.debug(list(size_me))
698 log.debug('--- global replacement sizer lines: ---')
699 for numba, replacement in sizers.items(): 699 ↛ 700line 699 didn't jump to line 700 because the loop on line 699 never started
700 log.debug(f'{numba} => {replacement}')
701 log.debug('---')
703 out = []
704 # next_slot = 0
705 punch_me = set(comment_outs)
706 for n, line in enumerate(incoming):
707 if n in punch_me:
708 corrected = f'%CONSIDERED_{line}'
709 out.append(corrected)
710 log.info(f' (x) Punched out line {n} -> ({corrected})')
711 continue
712 if str(n) in widen_me:
713 out.append(wideners[str(n)])
714 log.info(f' (<) Incoming: ({line})')
715 log.info(f' (>) Outgoing: ({wideners[str(n)]})')
716 continue
717 if str(n) in size_me: 717 ↛ 718line 717 didn't jump to line 718 because the condition on line 717 was never true
718 out.append(sizers[str(n)])
719 log.info(f' (<) Incoming: ({line})')
720 log.info(f' (>) Outgoing: ({sizers[str(n)]})')
721 continue
722 out.append(line)
724 # if next_slot < len(on_off_slots):
725 # trigger_on, trigger_off = on_off_slots[next_slot]
726 # tb = table_ranges[next_slot]
727 # else:
728 # trigger_on = None
729 # if trigger_on is None:
730 # out.append(line)
731 # continue
732 #
733 # if n < trigger_on:
734 # out.append(line)
735 # continue
736 # if n == trigger_on:
737 # out.append(TAB_NEW_START)
738 # out.append(TAB_HACKED_HEAD)
739 # continue
740 # if n <= tb['end_head']:
741 # continue
742 # if n < tb.get('bottom_rule', 0):
743 # out.append(line)
744 # if n in tb['end_data_row']: # type: ignore
745 # out.append(NEW_RULE)
746 # continue
747 # if tb.get('bottom_rule', 0) <= n < tb['amend']:
748 # continue
749 # if n == tb['amend']:
750 # out.append(TAB_NEW_END.replace('ANNOTATION', line))
751 # next_slot += 1
753 log.warning('Disabled naive table patching from before version 2023.2.12 for now')
755 if table_style == 'ugly':
756 tds_plain = r'\begin{longtable}[]{@{}'
757 tde_plain = r'@{}}'
758 col_placeholders = ('l', 'c', 'r')
759 ut_count = 0
760 udr_count = 0
761 in_table_data_rows = False
762 _out = []
763 for n, line in enumerate(out):
764 if line.startswith(tds_plain) and line.endswith(tde_plain): 764 ↛ 765line 764 didn't jump to line 765 because the condition on line 764 was never true
765 probe = line.strip().replace(tds_plain, '').replace(tde_plain, '')
766 robe = probe
767 for col_ph in col_placeholders:
768 robe = robe.replace(col_ph, '')
769 if not robe:
770 for col_ph in col_placeholders:
771 probe = probe.replace(col_ph, f'|{col_ph}')
772 line = tds_plain + probe + '|' + tde_plain
773 else:
774 log.warning(f'Encountered pseudo-plain table declaration ({line})')
775 if in_table_data_rows and line.endswith(r'\\'):
776 if not out[n + 1].startswith(r'\end{longtable}'):
777 line += r' \hline'
778 udr_count += 1
779 better_rules = (r'\toprule', r'\midrule', r'\bottomrule')
780 for rule in better_rules:
781 if rule in line:
782 if rule == r'\bottomrule':
783 ut_count += 1
784 line = line.replace(rule, r'\hline')
785 if rule == r'\toprule':
786 line += r'\rowcolor{light-gray}'
787 if line.startswith(r'\endlastfoot'):
788 in_table_data_rows = True
789 if line.startswith(r'\end{longtable}'):
790 in_table_data_rows = False
791 _out.append(line)
792 out = [line for line in _out]
793 del _out
794 sp_suffix = '' if ut_count == 1 else 's'
795 log.warning(f'Uglified horizontal rules in {ut_count} table{sp_suffix}')
796 sp_suffix = '' if udr_count == 1 else 's'
797 log.warning(f'Uglified a total of {udr_count} data row{sp_suffix}')
799 log.debug(' -----> ')
800 log.debug('# - - - 8< - - -')
801 log.debug(str('\n'.join(out)))
802 log.debug('# - - - 8< - - -')
804 return out