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