Coverage for liitos/tables.py: 78.05%

453 statements  

« prev     ^ index     » next       coverage.py v7.6.8, created at 2024-11-25 15:36:16 +00:00

1"""Apply all pairs in patch module on document.""" 

2 

3import re 

4from collections.abc import Iterable, Iterator 

5from typing import Union, no_type_check 

6 

7from liitos import log 

8 

9# The "target pattern for a line base minimal regex parser" 

10_ = r""" 

11\columns= 

12 

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""" 

50 

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}' 

58 

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""" 

66 

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""" 

91 

92NEW_RULE = r'\hline' 

93 

94TAB_NEW_END = r"""\end{longtable} 

95\end{small} 

96\vspace*{-2em} 

97\begin{footnotesize} 

98ANNOTATION 

99\end{footnotesize}""" 

100 

101COMMA = ',' 

102 

103 

104class Table: 

105 """Some adhoc structure to encapsulate the source and target table.""" 

106 

107 SourceMapType = list[tuple[int, str]] 

108 ColumnsType = dict[str, dict[str, Union[float, int, str]]] 

109 

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 

118 

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} \\' 

123 

124 LBP_MID_RULE_CONTEXT_STARTSWITH = r'\midrule\noalign{}' 

125 

126 LBP_END_FIRST_HEAD_STARTSWITH = r'\endfirsthead' 

127 

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} \\' 

132 

133 # LBP_MID_RULE_CONTEXT_STARTSWITH = r'\midrule\noalign{}' 

134 

135 LBP_END_ALL_HEAD_STARTSWITH = r'\endhead' 

136 

137 LBP_BOTTOM_RULE_CONTEXT_STARTSWITH = r'\bottomrule\noalign{}' 

138 

139 LBP_END_LAST_FOOT_STARTSWITH = r'\endlastfoot' 

140 

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'\\' 

143 

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}' 

148 

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() 

177 self.src_map.append((pos, line)) 

178 if line.startswith(Table.LBP_STARTSWITH_TAB_ENV_END): 

179 consumed = True 

180 

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}') 

187 

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 

210 

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 

214 

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 

218 

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 

222 

223 def column_source_widths(self) -> list[float]: 

224 """Return the incoming column widths.""" 

225 return self.source_widths 

226 

227 def column_target_widths(self) -> list[float]: 

228 """Return the outgoing column widths.""" 

229 return self.target_widths 

230 

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) 

235 

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 

239 

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): 248 ↛ 249line 248 didn't jump to line 249 because the condition on line 248 was never true

249 if len(self.source_widths): 

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 

263 

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 

267 

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() 

275 

276 def parse_column_widths(self) -> None: 

277 r"""Parse the column width declarations to initialize the columns data. 

278 

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 

318 

319 def parse_column_first_head(self) -> None: 

320 r"""Parse the head to extract the columns. 

321 

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): 343 ↛ 344line 343 didn't jump to line 344 because the condition on line 343 was never true

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: 354 ↛ 337line 354 didn't jump to line 337 because the condition on line 354 was always true

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 

361 

362 def parse_column_other_head(self) -> None: 

363 r"""Parse the other heads to extract the column labelss. 

364 

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 

406 

407 def parse_data_rows(self) -> None: 

408 r"""Parse the data rows. 

409 

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 

426 

427 

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, '' 

459 

460 

461def parse_columns_command(slot: int, text_line: str) -> tuple[bool, str, list[float]]: 

462 """Parse the \\columns=,0.2,0.7 command.""" 

463 if text_line.startswith(r'\columns='): 463 ↛ 477line 463 didn't jump to line 477 because the condition on line 463 was always true

464 log.info(f'trigger a columns mod for the next table environment at line #{slot + 1}|{text_line}') 

465 try: 

466 cols_csv = text_line.split('=', 1)[1].strip() # r'\columns = , 20\%,70\%' --> r', 20\%,70\%' 

467 cols = [v.strip() for v in cols_csv.split(COMMA)] 

468 widths = [float(v.replace(r'\%', '')) / 100 if r'\%' in v else (float(v) if v else 0) for v in cols] 

469 rest = round(1 - sum(round(w, 5) for w in widths), 5) 

470 widths = [v if v else rest for v in widths] 

471 log.info(f' -> parsed columns mod as | {" | ".join(str(round(v, 2)) for v in widths)} |') 

472 return True, '', widths 

473 except Exception as err: 

474 log.error(f'failed to parse columns values from {text_line.strip()} with err: {err}') 

475 return False, text_line, [] 

476 else: 

477 return False, text_line, [] 

478 

479 

480@no_type_check 

481def patch(incoming: Iterable[str], lookup: Union[dict[str, str], None] = None) -> list[str]: 

482 """Later alligator. \\columns=,0.2,0.7 as mandatory trigger""" 

483 table_style = 'readable' if lookup is None else lookup.get('table_style', 'readable') 

484 log.info(f'requested table style is ({table_style})') 

485 table_section, head, annotation = False, False, False 

486 table_ranges = [] 

487 guess_slot = 0 

488 table_range = {} 

489 has_column = False 

490 has_font_size = False 

491 widths: list[float] = [] 

492 font_size = '' 

493 comment_outs = [] 

494 for n, text in enumerate(incoming): 

495 if not table_section: 495 ↛ 518line 495 didn't jump to line 518 because the condition on line 495 was always true

496 if not has_font_size: 496 ↛ 500line 496 didn't jump to line 500 because the condition on line 496 was always true

497 has_font_size, text_line, font_size = parse_table_font_size_command(n, text) 

498 if has_font_size: 498 ↛ 499line 498 didn't jump to line 499 because the condition on line 498 was never true

499 comment_outs.append(n) 

500 if not has_font_size: 500 ↛ 503line 500 didn't jump to line 503 because the condition on line 500 was always true

501 continue 

502 

503 if not has_column: 

504 has_column, text_line, widths = parse_columns_command(n, text) 

505 if has_column: 

506 comment_outs.append(n) 

507 if not has_column: 

508 continue 

509 

510 if not text.startswith(TAB_START_TOK): 

511 continue 

512 table_range['start'] = n 

513 table_section = True 

514 head = True 

515 table_range['end_data_row'] = [] 

516 continue 

517 

518 if text.startswith(TOP_RULE): 

519 table_range['top_rule'] = n 

520 continue 

521 

522 if text.startswith(MID_RULE): 

523 table_range['mid_rule'] = n 

524 continue 

525 

526 if text.startswith(END_HEAD): 

527 table_range['end_head'] = n 

528 head = False 

529 continue 

530 

531 if not head and text.strip().endswith(END_DATA_ROW): 

532 table_range['end_data_row'].append(n) 

533 continue 

534 

535 if text.startswith(BOT_RULE): 

536 table_range['bottom_rule'] = n 

537 continue 

538 

539 if text.startswith(TAB_END_TOK): 

540 table_range['end'] = n 

541 annotation = True 

542 guess_slot = n + 2 

543 continue 

544 

545 if annotation and n == guess_slot: 

546 table_range['amend'] = n 

547 table_ranges.append(table_range) 

548 table_range = {} 

549 annotation, table_section = False, False 

550 

551 log.info(f'Detected {len(table_ranges)} tables (method from before version 2023.2.12):') 

552 for thing in table_ranges: 552 ↛ 553line 552 didn't jump to line 553 because the loop on line 552 never started

553 log.info(f'- {thing}') 

554 

555 tables_in, on_off_slots = [], [] 

556 for table in table_ranges: 556 ↛ 557line 556 didn't jump to line 557 because the loop on line 556 never started

557 from_here = table['start'] 

558 thru_there = table['amend'] 

559 log.info('Table:') 

560 log.info(f'-from {incoming[from_here]}') 

561 log.info(f'-thru {incoming[thru_there]}') 

562 on_off = (from_here, thru_there + 1) 

563 on_off_slots.append(on_off) 

564 tables_in.append((on_off, [line for line in incoming[on_off[0] : on_off[1]]])) 

565 

566 log.debug('# - - - 8< - - -') 

567 if tables_in: 567 ↛ 568line 567 didn't jump to line 568 because the condition on line 567 was never true

568 log.debug(str('\n'.join(tables_in[0][1]))) 

569 log.debug('# - - - 8< - - -') 

570 

571 reader = iter(incoming) 

572 tables = [] 

573 comment_outs = [] 

574 n = 0 

575 widths = [] 

576 font_size = '' 

577 for line in reader: 

578 log.debug(f'zero-based-line-no={n}, text=({line}) table-count={len(tables)}') 

579 if not line.startswith(Table.LBP_STARTSWITH_TAB_ENV_BEGIN): 

580 if line.startswith(r'\tablefontsize='): 580 ↛ 581line 580 didn't jump to line 581 because the condition on line 580 was never true

581 has_font_size, text_line, font_size = parse_table_font_size_command(n, line) 

582 log.info(f' + {has_font_size=}, {text_line=}, {font_size=}') 

583 if has_font_size: 

584 comment_outs.append(n) 

585 log.info(f'FONT-SIZE at <<{n}>>') 

586 if line.startswith(r'\columns='): 

587 has_column, text_line, widths = parse_columns_command(n, line) 

588 log.info(f' + {has_column=}, {text_line=}, {widths=}') 

589 if has_column: 589 ↛ 592line 589 didn't jump to line 592 because the condition on line 589 was always true

590 comment_outs.append(n) 

591 log.info(f'COLUMNS-WIDTH at <<{n}>>') 

592 n += 1 

593 else: 

594 table = Table(n, line, reader, widths, font_size, style=table_style) 

595 tables.append(table) 

596 n += len(tables[-1].source_map()) 

597 log.debug(f'- incremented n to {n}') 

598 log.debug(f'! next n (zero offset) is {n}') 

599 widths = [] 

600 font_size = '' 

601 

602 log.info('---') 

603 for n, table in enumerate(tables, start=1): 

604 log.info(f'Table #{n} (total width = {table.table_width()}):') 

605 for rank, column in table.column_data().items(): 

606 log.info(f'{rank} -> {column}') 

607 log.info(f'- source widths = {table.column_source_widths()}):') 

608 log.info(f'- target widths = {table.column_target_widths()}):') 

609 for numba, replacement in table.width_patches().items(): 

610 log.info(f'{numba} -> {replacement}') 

611 for anchor, text in table.data_row_seps(): 

612 log.info(f'[data-row-seps]:{anchor} -> {text}') 

613 log.info(f'= (fontsize command = "{table.font_size}"):') 

614 log.info('---') 

615 log.info(f'Comment out the following {len(comment_outs)} lines (zero based numbers) - punch:') 

616 for number in comment_outs: 

617 log.info(f'- {number}') 

618 

619 wideners = {} 

620 for table in tables: 

621 for numba, replacement in table.width_patches().items(): 

622 wideners[numba] = replacement 

623 widen_me = set(wideners) 

624 log.debug('widen me has:') 

625 log.debug(list(widen_me)) 

626 log.debug('--- global replacement width lines: ---') 

627 for numba, replacement in wideners.items(): 

628 log.debug(f'{numba} => {replacement}') 

629 log.debug('---') 

630 

631 sizers = {} 

632 for table in tables: 

633 if table.font_size: 633 ↛ 634line 633 didn't jump to line 634 because the condition on line 633 was never true

634 sizers[str(table.src_map[0][0])] = '\\begin{' + table.font_size + '}\n' + table.src_map[0][1] 

635 sizers[str(table.src_map[-1][0])] = table.src_map[-1][1] + '\n' + '\\end{' + table.font_size + '}' 

636 size_me = set(sizers) 

637 log.debug('size me has:') 

638 log.debug(list(size_me)) 

639 log.debug('--- global replacement sizer lines: ---') 

640 for numba, replacement in sizers.items(): 640 ↛ 641line 640 didn't jump to line 641 because the loop on line 640 never started

641 log.debug(f'{numba} => {replacement}') 

642 log.debug('---') 

643 

644 out = [] 

645 # next_slot = 0 

646 punch_me = set(comment_outs) 

647 for n, line in enumerate(incoming): 

648 if n in punch_me: 

649 corrected = f'%CONSIDERED_{line}' 

650 out.append(corrected) 

651 log.info(f' (x) Punched out line {n} -> ({corrected})') 

652 continue 

653 if str(n) in widen_me: 

654 out.append(wideners[str(n)]) 

655 log.info(f' (<) Incoming: ({line})') 

656 log.info(f' (>) Outgoing: ({wideners[str(n)]})') 

657 continue 

658 if str(n) in size_me: 658 ↛ 659line 658 didn't jump to line 659 because the condition on line 658 was never true

659 out.append(sizers[str(n)]) 

660 log.info(f' (<) Incoming: ({line})') 

661 log.info(f' (>) Outgoing: ({sizers[str(n)]})') 

662 continue 

663 out.append(line) 

664 

665 # if next_slot < len(on_off_slots): 

666 # trigger_on, trigger_off = on_off_slots[next_slot] 

667 # tb = table_ranges[next_slot] 

668 # else: 

669 # trigger_on = None 

670 # if trigger_on is None: 

671 # out.append(line) 

672 # continue 

673 # 

674 # if n < trigger_on: 

675 # out.append(line) 

676 # continue 

677 # if n == trigger_on: 

678 # out.append(TAB_NEW_START) 

679 # out.append(TAB_HACKED_HEAD) 

680 # continue 

681 # if n <= tb['end_head']: 

682 # continue 

683 # if n < tb.get('bottom_rule', 0): 

684 # out.append(line) 

685 # if n in tb['end_data_row']: # type: ignore 

686 # out.append(NEW_RULE) 

687 # continue 

688 # if tb.get('bottom_rule', 0) <= n < tb['amend']: 

689 # continue 

690 # if n == tb['amend']: 

691 # out.append(TAB_NEW_END.replace('ANNOTATION', line)) 

692 # next_slot += 1 

693 

694 log.warning('Disabled naive table patching from before version 2023.2.12 for now') 

695 

696 if table_style == 'ugly': 

697 tds_plain = r'\begin{longtable}[]{@{}' 

698 tde_plain = r'@{}}' 

699 col_placeholders = ('l', 'c', 'r') 

700 ut_count = 0 

701 udr_count = 0 

702 in_table_data_rows = False 

703 _out = [] 

704 for n, line in enumerate(out): 

705 if line.startswith(tds_plain) and line.endswith(tde_plain): 705 ↛ 706line 705 didn't jump to line 706 because the condition on line 705 was never true

706 probe = line.strip().replace(tds_plain, '').replace(tde_plain, '') 

707 robe = probe 

708 for col_ph in col_placeholders: 

709 robe = robe.replace(col_ph, '') 

710 if not robe: 

711 for col_ph in col_placeholders: 

712 probe = probe.replace(col_ph, f'|{col_ph}') 

713 line = tds_plain + probe + '|' + tde_plain 

714 else: 

715 log.warning(f'Encountered pseudo-plain table declaration ({line})') 

716 if in_table_data_rows and line.endswith(r'\\'): 

717 if not out[n + 1].startswith(r'\end{longtable}'): 

718 line += r' \hline' 

719 udr_count += 1 

720 better_rules = (r'\toprule', r'\midrule', r'\bottomrule') 

721 for rule in better_rules: 

722 if rule in line: 

723 if rule == r'\bottomrule': 

724 ut_count += 1 

725 line = line.replace(rule, r'\hline') 

726 if rule == r'\toprule': 

727 line += r'\rowcolor{light-gray}' 

728 if line.startswith(r'\endlastfoot'): 

729 in_table_data_rows = True 

730 if line.startswith(r'\end{longtable}'): 

731 in_table_data_rows = False 

732 _out.append(line) 

733 out = [line for line in _out] 

734 del _out 

735 sp_suffix = '' if ut_count == 1 else 's' 

736 log.warning(f'Uglified horizontal rules in {ut_count} table{sp_suffix}') 

737 sp_suffix = '' if udr_count == 1 else 's' 

738 log.warning(f'Uglified a total of {udr_count} data row{sp_suffix}') 

739 

740 log.debug(' -----> ') 

741 log.debug('# - - - 8< - - -') 

742 log.debug(str('\n'.join(out))) 

743 log.debug('# - - - 8< - - -') 

744 

745 return out