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

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

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

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 

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

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 

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 r"""Parse the \\columns=,0.2,0.7 command. 

463 

464 Examples: 

465 

466 >>> slot = 0 

467 >>> text = '\\columns=,0.2,0.7' 

468 >>> parse_columns_command(slot, text) 

469 (True, '', [0.1, 0.2, 0.7]) 

470 

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

475 

476 >>> slot = 0 

477 >>> text = '\\columns=0.4,0.7' 

478 >>> parse_columns_command(slot, text) 

479 (True, '', [0.4, 0.7]) 

480 

481 >>> slot = 0 

482 >>> text = r'\columns= , 20\%,70\%' 

483 >>> parse_columns_command(slot, text) 

484 (True, '', [0.1, 0.2, 0.7]) 

485 

486 >>> slot = 0 

487 >>> text = '\\columns====' 

488 >>> parse_columns_command(slot, text) 

489 (False, '\\columns====', []) 

490 

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, [] 

511 

512 

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 

516 

517 Examples: 

518 

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

522 

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 

561 

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 

568 

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 

576 

577 if text.startswith(TOP_RULE): 

578 table_range['top_rule'] = n 

579 continue 

580 

581 if text.startswith(MID_RULE): 

582 table_range['mid_rule'] = n 

583 continue 

584 

585 if text.startswith(END_HEAD): 

586 table_range['end_head'] = n 

587 head = False 

588 continue 

589 

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

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

592 continue 

593 

594 if text.startswith(BOT_RULE): 

595 table_range['bottom_rule'] = n 

596 continue 

597 

598 if text.startswith(TAB_END_TOK): 

599 table_range['end'] = n 

600 annotation = True 

601 guess_slot = n + 2 

602 continue 

603 

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 

609 

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

613 

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

624 

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

629 

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

660 

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

677 

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

689 

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

702 

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) 

723 

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 

752 

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

754 

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

798 

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

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

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

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

803 

804 return out