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

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

102EQ = '=' 

103SP = ' ' 

104 

105 

106class Table: 

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

108 

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

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

111 

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 

120 

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

125 

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

127 

128 LBP_END_FIRST_HEAD_STARTSWITH = r'\endfirsthead' 

129 

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

134 

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

136 

137 LBP_END_ALL_HEAD_STARTSWITH = r'\endhead' 

138 

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

140 

141 LBP_END_LAST_FOOT_STARTSWITH = r'\endlastfoot' 

142 

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

145 

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

150 

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 

182 

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

189 

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 

212 

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 

216 

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 

220 

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 

224 

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

226 """Return the incoming column widths.""" 

227 return self.source_widths 

228 

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

230 """Return the outgoing column widths.""" 

231 return self.target_widths 

232 

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) 

237 

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 

241 

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 

265 

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 

269 

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

277 

278 def parse_column_widths(self) -> None: 

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

280 

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 

320 

321 def parse_column_first_head(self) -> None: 

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

323 

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 

363 

364 def parse_column_other_head(self) -> None: 

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

366 

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 

408 

409 def parse_data_rows(self) -> None: 

410 r"""Parse the data rows. 

411 

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 

428 

429 

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

461 

462 

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. 

465 

466 Examples: 

467 

468 >>> slot = 0 

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

470 >>> parse_columns_command(slot, text) 

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

472 

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

477 

478 >>> slot = 0 

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

480 >>> parse_columns_command(slot, text) 

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

482 

483 >>> slot = 0 

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

485 >>> parse_columns_command(slot, text) 

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

487 

488 >>> slot = 0 

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

490 >>> parse_columns_command(slot, text) 

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

492 

493 >>> slot = 0 

494 >>> text = '\\columns====' 

495 >>> parse_columns_command(slot, text) 

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

497 

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

524 

525 

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 

529 

530 Examples: 

531 

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

535 

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 

574 

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 

581 

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 

589 

590 if text.startswith(TOP_RULE): 

591 table_range['top_rule'] = n 

592 continue 

593 

594 if text.startswith(MID_RULE): 

595 table_range['mid_rule'] = n 

596 continue 

597 

598 if text.startswith(END_HEAD): 

599 table_range['end_head'] = n 

600 head = False 

601 continue 

602 

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

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

605 continue 

606 

607 if text.startswith(BOT_RULE): 

608 table_range['bottom_rule'] = n 

609 continue 

610 

611 if text.startswith(TAB_END_TOK): 

612 table_range['end'] = n 

613 annotation = True 

614 guess_slot = n + 2 

615 continue 

616 

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 

622 

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

626 

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

637 

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

642 

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

673 

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

690 

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

702 

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

715 

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) 

736 

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 

765 

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

767 

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

811 

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

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

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

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

816 

817 return out