Coverage for src / pyTRLCConverter / rst_converter.py: 98%

264 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 12:06 +0000

1"""Converter to reStructuredText format. 

2 

3 Author: Gabryel Reyes (gabryel.reyes@newtec.de) 

4""" 

5 

6# pyTRLCConverter - A tool to convert TRLC files to specific formats. 

7# Copyright (c) 2024 - 2025 NewTec GmbH 

8# 

9# This file is part of pyTRLCConverter program. 

10# 

11# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software Foundation, 

13# either version 3 of the License, or (at your option) any later version. 

14# 

15# The pyTRLCConverter program is distributed in the hope that it will be useful, but 

16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

17# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License along with pyTRLCConverter. 

20# If not, see <https://www.gnu.org/licenses/>. 

21 

22# Imports ********************************************************************** 

23import os 

24from typing import List, Optional, Any 

25from marko import Markdown 

26from trlc.ast import Implicit_Null, Record_Object, Record_Reference, String_Literal, Expression 

27from pyTRLCConverter.base_converter import BaseConverter 

28from pyTRLCConverter.ret import Ret 

29from pyTRLCConverter.trlc_helper import TrlcAstWalker 

30from pyTRLCConverter.logger import log_verbose, log_error 

31from pyTRLCConverter.marko.rst_renderer import RSTRenderer 

32 

33# Variables ******************************************************************** 

34 

35# Classes ********************************************************************** 

36 

37class RstConverter(BaseConverter): 

38 """ 

39 RstConverter provides functionality for converting to a reStructuredText format. 

40 """ 

41 OUTPUT_FILE_NAME_DEFAULT = "output.rst" 

42 TOP_LEVEL_DEFAULT = "Specification" 

43 

44 def __init__(self, args: Any) -> None: 

45 # lobster-trace: SwRequirements.sw_req_rst 

46 """ 

47 Initializes the converter. 

48 

49 Args: 

50 args (Any): The parsed program arguments. 

51 """ 

52 super().__init__(args) 

53 

54 # The path to the given output folder. 

55 self._out_path = args.out 

56 

57 # The excluded paths in normalized form. 

58 self._excluded_paths = [] 

59 

60 if args.exclude is not None: 

61 self._excluded_paths = [os.path.normpath(path) for path in args.exclude] 

62 

63 # The file descriptor for the output file. 

64 self._fd = None 

65 

66 # The base level for the headings. Its the minimum level for the headings which depends 

67 # on the single/multiple document mode. 

68 self._base_level = 1 

69 

70 # For proper reStructuredText formatting, the first written part shall not have an empty line before. 

71 # But all following parts (heading, table, paragraph, image, etc.) shall have an empty line before. 

72 # And at the document bottom, there shall be just one empty line. 

73 self._empty_line_required = False 

74 

75 # The AST walker meta data for processing the record object fields. 

76 # This will hold the information about the current package, type and attribute being processed. 

77 self._ast_meta_data = None 

78 

79 @staticmethod 

80 def get_subcommand() -> str: 

81 # lobster-trace: SwRequirements.sw_req_rst 

82 """ 

83 Return subcommand token for this converter. 

84 

85 Returns: 

86 str: Parser subcommand token 

87 """ 

88 return "rst" 

89 

90 @staticmethod 

91 def get_description() -> str: 

92 # lobster-trace: SwRequirements.sw_req_rst 

93 """ 

94 Return converter description. 

95 

96 Returns: 

97 str: Converter description 

98 """ 

99 return "Convert into reStructuredText format." 

100 

101 @classmethod 

102 def register(cls, args_parser: Any) -> None: 

103 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

104 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

105 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_default 

106 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_custom 

107 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_default 

108 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_custom 

109 """ 

110 Register converter specific argument parser. 

111 

112 Args: 

113 args_parser (Any): Argument parser 

114 """ 

115 super().register(args_parser) 

116 

117 assert BaseConverter._parser is not None 

118 

119 BaseConverter._parser.add_argument( 

120 "-e", 

121 "--empty", 

122 type=str, 

123 default=BaseConverter.EMPTY_ATTRIBUTE_DEFAULT, 

124 required=False, 

125 help="Every attribute value which is empty will output the string " \ 

126 f"(default = {BaseConverter.EMPTY_ATTRIBUTE_DEFAULT})." 

127 ) 

128 

129 BaseConverter._parser.add_argument( 

130 "-n", 

131 "--name", 

132 type=str, 

133 default=RstConverter.OUTPUT_FILE_NAME_DEFAULT, 

134 required=False, 

135 help="Name of the generated output file inside the output folder " \ 

136 f"(default = {RstConverter.OUTPUT_FILE_NAME_DEFAULT}) in " \ 

137 "case a single document is generated." 

138 ) 

139 

140 BaseConverter._parser.add_argument( 

141 "-sd", 

142 "--single-document", 

143 action="store_true", 

144 required=False, 

145 default=False, 

146 help="Generate a single document instead of multiple files. The default is to generate multiple files." 

147 ) 

148 

149 BaseConverter._parser.add_argument( 

150 "-tl", 

151 "--top-level", 

152 type=str, 

153 default=RstConverter.TOP_LEVEL_DEFAULT, 

154 required=False, 

155 help="Name of the top level heading, required in single document mode " \ 

156 f"(default = {RstConverter.TOP_LEVEL_DEFAULT})." 

157 ) 

158 

159 def begin(self) -> Ret: 

160 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

161 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level 

162 """ 

163 Begin the conversion process. 

164 

165 Returns: 

166 Ret: Status 

167 """ 

168 assert self._fd is None 

169 

170 # Call the base converter to initialize the common stuff. 

171 result = BaseConverter.begin(self) 

172 

173 if result == Ret.OK: 

174 

175 # Single document mode? 

176 if self._args.single_document is True: 

177 log_verbose("Single document mode.") 

178 else: 

179 log_verbose("Multiple document mode.") 

180 

181 # Set the value for empty attributes. 

182 self._empty_attribute_value = self._args.empty 

183 

184 log_verbose(f"Empty attribute value: {self._empty_attribute_value}") 

185 

186 # Single document mode? 

187 if self._args.single_document is True: 

188 result = self._generate_out_file(self._args.name) 

189 

190 if self._fd is not None: 

191 self._write_empty_line_on_demand() 

192 self._fd.write(RstConverter.rst_create_heading(self._args.top_level, 1, self._args.name)) 

193 

194 # All headings will be shifted by one level. 

195 self._base_level = self._base_level + 1 

196 

197 return result 

198 

199 def enter_file(self, file_name: str) -> Ret: 

200 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

201 """ 

202 Enter a file. 

203 

204 Args: 

205 file_name (str): File name 

206  

207 Returns: 

208 Ret: Status 

209 """ 

210 result = Ret.OK 

211 

212 # Multiple document mode? 

213 if self._args.single_document is False: 

214 assert self._fd is None 

215 

216 file_name_rst = self._file_name_trlc_to_rst(file_name) 

217 result = self._generate_out_file(file_name_rst) 

218 

219 # The very first written reStructuredText part shall not have an empty line before. 

220 self._empty_line_required = False 

221 

222 return result 

223 

224 def leave_file(self, file_name: str) -> Ret: 

225 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

226 """ 

227 Leave a file. 

228 

229 Args: 

230 file_name (str): File name 

231 

232 Returns: 

233 Ret: Status 

234 """ 

235 

236 # Multiple document mode? 

237 if self._args.single_document is False: 

238 assert self._fd is not None 

239 self._fd.close() 

240 self._fd = None 

241 

242 return Ret.OK 

243 

244 def convert_section(self, section: str, level: int) -> Ret: 

245 # lobster-trace: SwRequirements.sw_req_rst_section 

246 """ 

247 Process the given section item. 

248 It will create a reStructuredText heading with the given section name and level. 

249 

250 Args: 

251 section (str): The section name 

252 level (int): The section indentation level 

253  

254 Returns: 

255 Ret: Status 

256 """ 

257 assert len(section) > 0 

258 assert self._fd is not None 

259 

260 self._write_empty_line_on_demand() 

261 rst_heading = self.rst_create_heading(section, 

262 self._get_rst_heading_level(level), 

263 os.path.basename(self._fd.name)) 

264 self._fd.write(rst_heading) 

265 

266 return Ret.OK 

267 

268 def convert_record_object_generic(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret: 

269 # lobster-trace: SwRequirements.sw_req_rst_record 

270 """ 

271 Process the given record object in a generic way. 

272 

273 The handler is called by the base converter if no specific handler is 

274 defined for the record type. 

275 

276 Args: 

277 record (Record_Object): The record object. 

278 level (int): The record level. 

279 translation (Optional[dict]): Translation dictionary for the record object. 

280 If None, no translation is applied. 

281  

282 Returns: 

283 Ret: Status 

284 """ 

285 assert self._fd is not None 

286 

287 self._write_empty_line_on_demand() 

288 

289 return self._convert_record_object(record, level, translation) 

290 

291 def finish(self): 

292 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

293 """ 

294 Finish the conversion process. 

295 """ 

296 

297 # Single document mode? 

298 if self._args.single_document is True: 

299 assert self._fd is not None 

300 self._fd.close() 

301 self._fd = None 

302 

303 return Ret.OK 

304 

305 def _write_empty_line_on_demand(self) -> None: 

306 # lobster-trace: SwRequirements.sw_req_rst 

307 """ 

308 Write an empty line if necessary. 

309 

310 For proper reStructuredText formatting, the first written part shall not have an empty 

311 line before. But all following parts (heading, table, paragraph, image, etc.) shall 

312 have an empty line before. And at the document bottom, there shall be just one empty 

313 line. 

314 """ 

315 assert self._fd is not None 

316 

317 if self._empty_line_required is False: 

318 self._empty_line_required = True 

319 else: 

320 self._fd.write("\n") 

321 

322 def _get_rst_heading_level(self, level: int) -> int: 

323 # lobster-trace: SwRequirements.sw_req_rst_section 

324 """ 

325 Get the reStructuredText heading level from the TRLC object level. 

326 Its mandatory to use this method to calculate the reStructuredText heading level. 

327 Otherwise in single document mode the top level heading will be wrong. 

328 

329 Args: 

330 level (int): The TRLC object level. 

331  

332 Returns: 

333 int: reStructuredText heading level 

334 """ 

335 return self._base_level + level 

336 

337 def _file_name_trlc_to_rst(self, file_name_trlc: str) -> str: 

338 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

339 """ 

340 Convert a TRLC file name to a reStructuredText file name. 

341 

342 Args: 

343 file_name_trlc (str): TRLC file name 

344  

345 Returns: 

346 str: reStructuredText file name 

347 """ 

348 file_name = os.path.basename(file_name_trlc) 

349 file_name = os.path.splitext(file_name)[0] + ".rst" 

350 

351 return file_name 

352 

353 def _generate_out_file(self, file_name: str) -> Ret: 

354 # lobster-trace: SwRequirements.sw_req_rst_out_folder 

355 """ 

356 Generate the output file. 

357 

358 Args: 

359 file_name (str): The output file name without path. 

360 item_list ([Element]): List of elements. 

361 

362 Returns: 

363 Ret: Status 

364 """ 

365 result = Ret.OK 

366 file_name_with_path = file_name 

367 

368 # Add path to the output file name. 

369 if 0 < len(self._out_path): 

370 file_name_with_path = os.path.join(self._out_path, file_name) 

371 

372 try: 

373 self._fd = open(file_name_with_path, "w", encoding="utf-8") #pylint: disable=consider-using-with 

374 except IOError as e: 

375 log_error(f"Failed to open file {file_name_with_path}: {e}") 

376 result = Ret.ERROR 

377 

378 return result 

379 

380 def _on_implict_null(self, _: Implicit_Null) -> str: 

381 # lobster-trace: SwRequirements.sw_req_rst_record 

382 """ 

383 Process the given implicit null value. 

384  

385 Returns: 

386 str: The implicit null value. 

387 """ 

388 return self.rst_escape(self._empty_attribute_value) 

389 

390 def _on_record_reference(self, record_reference: Record_Reference) -> str: 

391 # lobster-trace: SwRequirements.sw_req_rst_record 

392 """ 

393 Process the given record reference value and return a reStructuredText link. 

394 

395 Args: 

396 record_reference (Record_Reference): The record reference value. 

397  

398 Returns: 

399 str: reStructuredText link to the record reference. 

400 """ 

401 return self._create_rst_link_from_record_object_reference(record_reference) 

402 

403 def _on_string_literal(self, string_literal: String_Literal) -> str: 

404 # lobster-trace: SwReq sw_req_rst_string_format 

405 # lobster-trace: SwRequirements.sw_req_rst_render_md 

406 """ 

407 Process the given string literal value. 

408 

409 Args: 

410 string_literal (String_Literal): The string literal value. 

411  

412 Returns: 

413 str: The string literal value. 

414 """ 

415 result = string_literal.to_string() 

416 

417 if self._ast_meta_data is not None: 

418 package_name = self._ast_meta_data.get("package_name", "") 

419 type_name = self._ast_meta_data.get("type_name", "") 

420 attribute_name = self._ast_meta_data.get("attribute_name", "") 

421 

422 result = self._render(package_name, type_name, attribute_name, result) 

423 

424 return result 

425 

426 def _create_rst_link_from_record_object_reference(self, record_reference: Record_Reference) -> str: 

427 # lobster-trace: SwRequirements.sw_req_rst_link 

428 """ 

429 Create a reStructuredText cross-reference from a record reference. 

430 It considers the file name, the package name, and the record name. 

431 

432 Args: 

433 record_reference (Record_Reference): Record reference 

434 

435 Returns: 

436 str: reStructuredText cross-reference 

437 """ 

438 assert record_reference.target is not None 

439 

440 file_name = "" 

441 

442 # Single document mode? 

443 if self._args.single_document is True: 

444 file_name = self._args.name 

445 

446 # Is the link to a excluded file? 

447 for excluded_path in self._excluded_paths: 

448 

449 if os.path.commonpath([excluded_path, record_reference.target.location.file_name]) == excluded_path: 

450 file_name = self._file_name_trlc_to_rst(record_reference.target.location.file_name) 

451 break 

452 

453 # Multiple document mode 

454 else: 

455 file_name = self._file_name_trlc_to_rst(record_reference.target.location.file_name) 

456 

457 record_name = record_reference.target.name 

458 

459 # Create a target ID for the record 

460 target_id = f"{file_name}-{record_name.lower().replace(' ', '-')}" 

461 

462 return RstConverter.rst_create_link(str(record_reference.to_python_object()), target_id) 

463 

464 def _other_dispatcher(self, expression: Expression) -> str: 

465 """ 

466 Dispatcher for all other expressions. 

467 

468 Args: 

469 expression (Expression): The expression to process. 

470 

471 Returns: 

472 str: The processed expression. 

473 """ 

474 return self.rst_escape(expression.to_string()) 

475 

476 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

477 # lobster-trace: SwRequirements.sw_req_rst_record 

478 # lobster-trace: SwRequirements.sw_req_rst_escape 

479 # lobster-trace: SwReq sw_req_rst_string_format 

480 """ 

481 If a record object contains a record reference, the record reference will be converted to 

482 a Markdown link. 

483 If a record object contains an array of record references, the array will be converted to 

484 a reStructuredText list of links. 

485 Otherwise the record object fields attribute values will be written to the reStructuredText table. 

486 

487 Returns: 

488 TrlcAstWalker: The TRLC AST walker. 

489 """ 

490 trlc_ast_walker = TrlcAstWalker() 

491 trlc_ast_walker.add_dispatcher( 

492 Implicit_Null, 

493 None, 

494 self._on_implict_null, 

495 None 

496 ) 

497 trlc_ast_walker.add_dispatcher( 

498 Record_Reference, 

499 None, 

500 self._on_record_reference, 

501 None 

502 ) 

503 trlc_ast_walker.add_dispatcher( 

504 String_Literal, 

505 None, 

506 self._on_string_literal, 

507 None 

508 ) 

509 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher) 

510 

511 return trlc_ast_walker 

512 

513 def _render(self, package_name: str, type_name: str, attribute_name: str, attribute_value: str) -> str: 

514 # lobster-trace: SwRequirements.sw_req_rst_string_format 

515 # lobster-trace: SwRequirements.sw_req_rst_render_md 

516 """Render the attribute value depened on its format. 

517 

518 Args: 

519 package_name (str): The package name. 

520 type_name (str): The type name. 

521 attribute_name (str): The attribute name. 

522 attribute_value (str): The attribute value. 

523 

524 Returns: 

525 str: The rendered attribute value. 

526 """ 

527 result = attribute_value 

528 

529 # If the attribute value is not already in reStructuredText format, it will be escaped. 

530 if self._render_cfg.is_format_rst(package_name, type_name, attribute_name) is False: 

531 

532 # Is it Markdown format? 

533 if self._render_cfg.is_format_md(package_name, type_name, attribute_name) is True: 

534 # Convert Markdown to reStructuredText. 

535 markdown = Markdown(renderer=RSTRenderer) 

536 result = markdown.convert(attribute_value) 

537 

538 # Otherwise escape the text for reStructuredText. 

539 else: 

540 result = self.rst_escape(attribute_value) 

541 

542 return result 

543 

544 # pylint: disable-next=too-many-locals, unused-argument 

545 def _convert_record_object(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret: 

546 # lobster-trace: SwRequirements.sw_req_rst_record 

547 """ 

548 Process the given record object. 

549 

550 Args: 

551 record (Record_Object): The record object. 

552 level (int): The record level. 

553 translation (Optional[dict]): Translation dictionary for the record object. 

554 If None, no translation is applied. 

555  

556 Returns: 

557 Ret: Status 

558 """ 

559 assert self._fd is not None 

560 

561 # The record name will be the admonition. 

562 file_name = os.path.basename(self._fd.name) 

563 rst_heading = self.rst_create_admonition(record.name, 

564 file_name) 

565 self._fd.write(rst_heading) 

566 

567 self._write_empty_line_on_demand() 

568 

569 # The record fields will be written to a table. 

570 column_titles = ["Attribute Name", "Attribute Value"] 

571 

572 # Build rows for the table. 

573 # Its required to calculate the maximum width for each column, therefore the rows 

574 # will be stored first in a list and then the maximum width will be calculated. 

575 # The table will be written after the maximum width calculation. 

576 rows = [] 

577 trlc_ast_walker = self._get_trlc_ast_walker() 

578 for name, value in record.field.items(): 

579 attribute_name = self._translate_attribute_name(translation, name) 

580 attribute_name = self.rst_escape(attribute_name) 

581 

582 # Retrieve the attribute value by processing the field value. 

583 # The result will be a string representation of the value. 

584 # If the value is an array of record references, the result will be a Markdown list of links. 

585 # If the value is a single record reference, the result will be a Markdown link. 

586 # If the value is a string literal, the result will be the string literal value that considers 

587 # its formatting. 

588 # Otherwise the result will be the attribute value in a proper format. 

589 self._ast_meta_data = { 

590 "package_name": record.n_package.name, 

591 "type_name": record.n_typ.name, 

592 "attribute_name": name 

593 } 

594 walker_result = trlc_ast_walker.walk(value) 

595 

596 attribute_value = "" 

597 if isinstance(walker_result, list): 

598 attribute_value = self.rst_create_list(walker_result, False) 

599 else: 

600 attribute_value = walker_result 

601 

602 rows.append([attribute_name, attribute_value]) 

603 

604 # Calculate the maximum width of each column based on both headers and row values. 

605 max_widths = [len(title) for title in column_titles] 

606 for row in rows: 

607 for idx, value in enumerate(row): 

608 lines = value.split('\n') 

609 for line in lines: 

610 max_widths[idx] = max(max_widths[idx], len(line)) 

611 

612 # Write the table head and rows. 

613 rst_table_head = self.rst_create_table_head(column_titles, max_widths) 

614 self._fd.write(rst_table_head) 

615 

616 for row in rows: 

617 rst_table_row = self.rst_append_table_row(row, max_widths, False) 

618 self._fd.write(rst_table_row) 

619 

620 return Ret.OK 

621 

622 @staticmethod 

623 def rst_escape(text: str) -> str: 

624 # lobster-trace: SwRequirements.sw_req_rst_escape 

625 """ 

626 Escapes the text to be used in a reStructuredText document. 

627 

628 Args: 

629 text (str): Text to escape 

630 

631 Returns: 

632 str: Escaped text 

633 """ 

634 characters = ["\\", "`", "*", "_", "{", "}", "[", "]", "<", ">", "(", ")", "#", "+", "-", ".", "!", "|"] 

635 

636 for character in characters: 

637 text = text.replace(character, "\\" + character) 

638 

639 return text 

640 

641 @staticmethod 

642 def rst_create_heading(text: str, 

643 level: int, 

644 file_name: str, 

645 escape: bool = True) -> str: 

646 # lobster-trace: SwRequirements.sw_req_rst_heading 

647 """ 

648 Create a reStructuredText heading with a label. 

649 The text will be automatically escaped for reStructuredText if necessary. 

650 

651 Args: 

652 text (str): Heading text 

653 level (int): Heading level [1; 7] 

654 file_name (str): File name where the heading is found 

655 escape (bool): Escape the text (default: True). 

656 

657 Returns: 

658 str: reStructuredText heading with a label 

659 """ 

660 result = "" 

661 

662 if 1 <= level <= 7: 

663 text_raw = text 

664 

665 if escape is True: 

666 text_raw = RstConverter.rst_escape(text) 

667 

668 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}" 

669 

670 underline_char = ["=", "#", "~", "^", "\"", "+", "'"][level - 1] 

671 underline = underline_char * len(text_raw) 

672 

673 result = f".. _{label}:\n\n{text_raw}\n{underline}\n" 

674 

675 else: 

676 log_error(f"Invalid heading level {level} for {text}.") 

677 

678 return result 

679 

680 @staticmethod 

681 def rst_create_admonition(text: str, 

682 file_name: str, 

683 escape: bool = True) -> str: 

684 # lobster-trace: SwRequirements.sw_req_rst_admonition 

685 """ 

686 Create a reStructuredText admonition with a label. 

687 The text will be automatically escaped for reStructuredText if necessary. 

688 

689 Args: 

690 text (str): Admonition text 

691 file_name (str): File name where the heading is found 

692 escape (bool): Escape the text (default: True). 

693 

694 Returns: 

695 str: reStructuredText admonition with a label 

696 """ 

697 text_raw = text 

698 

699 if escape is True: 

700 text_raw = RstConverter.rst_escape(text) 

701 

702 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}" 

703 admonition_label = f".. admonition:: {text_raw}" 

704 

705 return f".. _{label}:\n\n{admonition_label}\n" 

706 

707 @staticmethod 

708 def rst_create_table_head(column_titles: List[str], max_widths: List[int], escape: bool = True) -> str: 

709 # lobster-trace: SwRequirements.sw_req_rst_table 

710 """ 

711 Create the table head for a reStructuredText table in grid format. 

712 The titles will be automatically escaped for reStructuredText if necessary. 

713 

714 Args: 

715 column_titles ([str]): List of column titles. 

716 max_widths ([int]): List of maximum widths for each column. 

717 escape (bool): Escape the titles (default: True). 

718 

719 Returns: 

720 str: Table head 

721 """ 

722 if escape: 

723 column_titles = [RstConverter.rst_escape(title) for title in column_titles] 

724 

725 # Create the top border of the table 

726 table_head = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n" 

727 

728 # Create the title row 

729 table_head += " |" 

730 table_head += "|".join([f" {title.ljust(max_widths[idx])} " for idx, title in enumerate(column_titles)]) + "|\n" 

731 

732 # Create the separator row 

733 table_head += " +" + "+".join(["=" * (width + 2) for width in max_widths]) + "+\n" 

734 

735 return table_head 

736 

737 @staticmethod 

738 def rst_append_table_row(row_values: List[str], max_widths: List[int], escape: bool = True) -> str: 

739 # lobster-trace: SwRequirements.sw_req_rst_table 

740 """ 

741 Append a row to a reStructuredText table in grid format. 

742 The values will be automatically escaped for reStructuredText if necessary. 

743 Supports multi-line cell values. 

744 

745 Args: 

746 row_values ([str]): List of row values. 

747 max_widths ([int]): List of maximum widths for each column. 

748 escape (bool): Escapes every row value (default: True). 

749 

750 Returns: 

751 str: Table row 

752 """ 

753 if escape: 

754 row_values = [RstConverter.rst_escape(value) for value in row_values] 

755 

756 # Split each cell value into lines. 

757 split_values = [value.split('\n') for value in row_values] 

758 max_lines = max(len(lines) for lines in split_values) 

759 

760 # Create the row with multi-line support. 

761 table_row = "" 

762 for line_idx in range(max_lines): 

763 table_row += " |" 

764 for col_idx, lines in enumerate(split_values): 

765 if line_idx < len(lines): 

766 table_row += f" {lines[line_idx].ljust(max_widths[col_idx])} " 

767 else: 

768 table_row += " " * (max_widths[col_idx] + 2) 

769 table_row += "|" 

770 table_row += "\n" 

771 

772 # Create the separator row. 

773 separator_row = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n" 

774 

775 return table_row + separator_row 

776 

777 @staticmethod 

778 def rst_create_list(list_values: List[str], escape: bool = True) -> str: 

779 # lobster-trace: SwRequirements.sw_req_rst_list 

780 """Create a unordered reStructuredText list. 

781 The values will be automatically escaped for reStructuredText if necessary. 

782 

783 Args: 

784 list_values (List[str]): List of list values. 

785 escape (bool): Escapes every list value (default: True). 

786  

787 Returns: 

788 str: reStructuredText list 

789 """ 

790 list_str = "" 

791 

792 for idx, value_raw in enumerate(list_values): 

793 value = value_raw 

794 

795 if escape is True: # Escape the value if necessary. 

796 value = RstConverter.rst_escape(value) 

797 

798 list_str += f"* {value}" 

799 

800 # The last list value must not have a newline at the end. 

801 if idx < len(list_values) - 1: 

802 list_str += "\n" 

803 

804 return list_str 

805 

806 @staticmethod 

807 def rst_create_link(text: str, target: str, escape: bool = True) -> str: 

808 # lobster-trace: SwRequirements.sw_req_rst_link 

809 """ 

810 Create a reStructuredText cross-reference. 

811 The text will be automatically escaped for reStructuredText if necessary. 

812 There will be no newline appended at the end. 

813 

814 Args: 

815 text (str): Link text 

816 target (str): Cross-reference target 

817 escape (bool): Escapes text (default: True). 

818 

819 Returns: 

820 str: reStructuredText cross-reference 

821 """ 

822 text_raw = text 

823 

824 if escape is True: 

825 text_raw = RstConverter.rst_escape(text) 

826 

827 return f":ref:`{text_raw} <{target}>`" 

828 

829 @staticmethod 

830 def rst_create_diagram_link(diagram_file_name: str, diagram_caption: str, escape: bool = True) -> str: 

831 # lobster-trace: SwRequirements.sw_req_rst_image 

832 """ 

833 Create a reStructuredText diagram link. 

834 The caption will be automatically escaped for reStructuredText if necessary. 

835 

836 Args: 

837 diagram_file_name (str): Diagram file name 

838 diagram_caption (str): Diagram caption 

839 escape (bool): Escapes caption (default: True). 

840 

841 Returns: 

842 str: reStructuredText diagram link 

843 """ 

844 diagram_caption_raw = diagram_caption 

845 

846 if escape is True: 

847 diagram_caption_raw = RstConverter.rst_escape(diagram_caption) 

848 

849 # Allowed are absolute and relative to source paths. 

850 diagram_file_name = os.path.normpath(diagram_file_name) 

851 

852 result = f".. figure:: {diagram_file_name}\n :alt: {diagram_caption_raw}\n" 

853 

854 if diagram_caption_raw: 

855 result += f"\n {diagram_caption_raw}\n" 

856 

857 return result 

858 

859 @staticmethod 

860 def rst_role(text: str, role: str, escape: bool = True) -> str: 

861 # lobster-trace: SwRequirements.sw_req_rst_role 

862 """ 

863 Create role text in reStructuredText. 

864 The text will be automatically escaped for reStructuredText if necessary. 

865 There will be no newline appended at the end. 

866 

867 Args: 

868 text (str): Text 

869 color (str): Role 

870 escape (bool): Escapes text (default: True). 

871 

872 Returns: 

873 str: Text with role 

874 """ 

875 text_raw = text 

876 

877 if escape is True: 

878 text_raw = RstConverter.rst_escape(text) 

879 

880 return f":{role}:`{text_raw}`" 

881 

882# Functions ******************************************************************** 

883 

884# Main *************************************************************************