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

268 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 12:20 +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 - 2026 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.md2rst_renderer import Md2RstRenderer 

32from pyTRLCConverter.marko.gfm2rst_renderer import Gfm2RstRenderer 

33 

34# Variables ******************************************************************** 

35 

36# Classes ********************************************************************** 

37 

38class RstConverter(BaseConverter): 

39 """ 

40 RstConverter provides functionality for converting to a reStructuredText format. 

41 """ 

42 OUTPUT_FILE_NAME_DEFAULT = "output.rst" 

43 TOP_LEVEL_DEFAULT = "Specification" 

44 

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

46 # lobster-trace: SwRequirements.sw_req_rst 

47 """ 

48 Initializes the converter. 

49 

50 Args: 

51 args (Any): The parsed program arguments. 

52 """ 

53 super().__init__(args) 

54 

55 # The path to the given output folder. 

56 self._out_path = args.out 

57 

58 # The excluded paths in normalized form. 

59 self._excluded_paths = [] 

60 

61 if args.exclude is not None: 

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

63 

64 # The file descriptor for the output file. 

65 self._fd = None 

66 

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

68 # on the single/multiple document mode. 

69 self._base_level = 1 

70 

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

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

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

74 self._empty_line_required = False 

75 

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

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

78 self._ast_meta_data = None 

79 

80 @staticmethod 

81 def get_subcommand() -> str: 

82 # lobster-trace: SwRequirements.sw_req_rst 

83 """ 

84 Return subcommand token for this converter. 

85 

86 Returns: 

87 str: Parser subcommand token 

88 """ 

89 return "rst" 

90 

91 @staticmethod 

92 def get_description() -> str: 

93 # lobster-trace: SwRequirements.sw_req_rst 

94 """ 

95 Return converter description. 

96 

97 Returns: 

98 str: Converter description 

99 """ 

100 return "Convert into reStructuredText format." 

101 

102 @classmethod 

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

104 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

105 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

106 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_default 

107 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_custom 

108 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_default 

109 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_custom 

110 """ 

111 Register converter specific argument parser. 

112 

113 Args: 

114 args_parser (Any): Argument parser 

115 """ 

116 super().register(args_parser) 

117 

118 assert BaseConverter._parser is not None 

119 

120 BaseConverter._parser.add_argument( 

121 "-e", 

122 "--empty", 

123 type=str, 

124 default=BaseConverter.EMPTY_ATTRIBUTE_DEFAULT, 

125 required=False, 

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

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

128 ) 

129 

130 BaseConverter._parser.add_argument( 

131 "-n", 

132 "--name", 

133 type=str, 

134 default=RstConverter.OUTPUT_FILE_NAME_DEFAULT, 

135 required=False, 

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

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

138 "case a single document is generated." 

139 ) 

140 

141 BaseConverter._parser.add_argument( 

142 "-sd", 

143 "--single-document", 

144 action="store_true", 

145 required=False, 

146 default=False, 

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

148 ) 

149 

150 BaseConverter._parser.add_argument( 

151 "-tl", 

152 "--top-level", 

153 type=str, 

154 default=RstConverter.TOP_LEVEL_DEFAULT, 

155 required=False, 

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

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

158 ) 

159 

160 def begin(self) -> Ret: 

161 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

162 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level 

163 """ 

164 Begin the conversion process. 

165 

166 Returns: 

167 Ret: Status 

168 """ 

169 assert self._fd is None 

170 

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

172 result = BaseConverter.begin(self) 

173 

174 if result == Ret.OK: 

175 

176 # Single document mode? 

177 if self._args.single_document is True: 

178 log_verbose("Single document mode.") 

179 else: 

180 log_verbose("Multiple document mode.") 

181 

182 # Set the value for empty attributes. 

183 self._empty_attribute_value = self._args.empty 

184 

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

186 

187 # Single document mode? 

188 if self._args.single_document is True: 

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

190 

191 if self._fd is not None: 

192 self._write_empty_line_on_demand() 

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

194 

195 # All headings will be shifted by one level. 

196 self._base_level = self._base_level + 1 

197 

198 return result 

199 

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

201 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

202 """ 

203 Enter a file. 

204 

205 Args: 

206 file_name (str): File name 

207  

208 Returns: 

209 Ret: Status 

210 """ 

211 result = Ret.OK 

212 

213 # Multiple document mode? 

214 if self._args.single_document is False: 

215 assert self._fd is None 

216 

217 file_name_rst = self._file_name_trlc_to_rst(file_name) 

218 result = self._generate_out_file(file_name_rst) 

219 

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

221 self._empty_line_required = False 

222 

223 return result 

224 

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

226 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

227 """ 

228 Leave a file. 

229 

230 Args: 

231 file_name (str): File name 

232 

233 Returns: 

234 Ret: Status 

235 """ 

236 

237 # Multiple document mode? 

238 if self._args.single_document is False: 

239 assert self._fd is not None 

240 self._fd.close() 

241 self._fd = None 

242 

243 return Ret.OK 

244 

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

246 # lobster-trace: SwRequirements.sw_req_rst_section 

247 """ 

248 Process the given section item. 

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

250 

251 Args: 

252 section (str): The section name 

253 level (int): The section indentation level 

254  

255 Returns: 

256 Ret: Status 

257 """ 

258 assert len(section) > 0 

259 assert self._fd is not None 

260 

261 self._write_empty_line_on_demand() 

262 rst_heading = self.rst_create_heading(section, 

263 self._get_rst_heading_level(level), 

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

265 self._fd.write(rst_heading) 

266 

267 return Ret.OK 

268 

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

270 # lobster-trace: SwRequirements.sw_req_rst_record 

271 """ 

272 Process the given record object in a generic way. 

273 

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

275 defined for the record type. 

276 

277 Args: 

278 record (Record_Object): The record object. 

279 level (int): The record level. 

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

281 If None, no translation is applied. 

282  

283 Returns: 

284 Ret: Status 

285 """ 

286 assert self._fd is not None 

287 

288 self._write_empty_line_on_demand() 

289 

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

291 

292 def finish(self): 

293 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

294 """ 

295 Finish the conversion process. 

296 """ 

297 

298 # Single document mode? 

299 if self._args.single_document is True: 

300 assert self._fd is not None 

301 self._fd.close() 

302 self._fd = None 

303 

304 return Ret.OK 

305 

306 def _write_empty_line_on_demand(self) -> None: 

307 # lobster-trace: SwRequirements.sw_req_rst 

308 """ 

309 Write an empty line if necessary. 

310 

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

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

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

314 line. 

315 """ 

316 assert self._fd is not None 

317 

318 if self._empty_line_required is False: 

319 self._empty_line_required = True 

320 else: 

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

322 

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

324 # lobster-trace: SwRequirements.sw_req_rst_section 

325 """ 

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

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

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

329 

330 Args: 

331 level (int): The TRLC object level. 

332  

333 Returns: 

334 int: reStructuredText heading level 

335 """ 

336 return self._base_level + level 

337 

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

339 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

340 """ 

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

342 

343 Args: 

344 file_name_trlc (str): TRLC file name 

345  

346 Returns: 

347 str: reStructuredText file name 

348 """ 

349 file_name = os.path.basename(file_name_trlc) 

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

351 

352 return file_name 

353 

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

355 # lobster-trace: SwRequirements.sw_req_rst_out_folder 

356 """ 

357 Generate the output file. 

358 

359 Args: 

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

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

362 

363 Returns: 

364 Ret: Status 

365 """ 

366 result = Ret.OK 

367 file_name_with_path = file_name 

368 

369 # Add path to the output file name. 

370 if 0 < len(self._out_path): 

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

372 

373 try: 

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

375 except IOError as e: 

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

377 result = Ret.ERROR 

378 

379 return result 

380 

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

382 # lobster-trace: SwRequirements.sw_req_rst_record 

383 """ 

384 Process the given implicit null value. 

385  

386 Returns: 

387 str: The implicit null value. 

388 """ 

389 return self.rst_escape(self._empty_attribute_value) 

390 

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

392 # lobster-trace: SwRequirements.sw_req_rst_record 

393 """ 

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

395 

396 Args: 

397 record_reference (Record_Reference): The record reference value. 

398  

399 Returns: 

400 str: reStructuredText link to the record reference. 

401 """ 

402 return self._create_rst_link_from_record_object_reference(record_reference) 

403 

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

405 # lobster-trace: SwRequirements.sw_req_rst_string_format 

406 # lobster-trace: SwRequirements.sw_req_rst_render_md 

407 # lobster-trace: SwRequirements.sw_req_rst_render_gfm 

408 """ 

409 Process the given string literal value. 

410 

411 Args: 

412 string_literal (String_Literal): The string literal value. 

413  

414 Returns: 

415 str: The string literal value. 

416 """ 

417 result = string_literal.to_string() 

418 

419 if self._ast_meta_data is not None: 

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

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

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

423 

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

425 

426 return result 

427 

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

429 # lobster-trace: SwRequirements.sw_req_rst_link 

430 """ 

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

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

433 

434 Args: 

435 record_reference (Record_Reference): Record reference 

436 

437 Returns: 

438 str: reStructuredText cross-reference 

439 """ 

440 assert record_reference.target is not None 

441 

442 file_name = "" 

443 

444 # Single document mode? 

445 if self._args.single_document is True: 

446 file_name = self._args.name 

447 

448 # Is the link to a excluded file? 

449 for excluded_path in self._excluded_paths: 

450 

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

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

453 break 

454 

455 # Multiple document mode 

456 else: 

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

458 

459 record_name = record_reference.target.name 

460 

461 # Create a target ID for the record 

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

463 

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

465 

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

467 # lobster-trace: SwRequirements.sw_req_rst_record 

468 # lobster-trace: SwRequirements.sw_req_rst_escape 

469 """ 

470 Dispatcher for all other expressions. 

471 

472 Args: 

473 expression (Expression): The expression to process. 

474 

475 Returns: 

476 str: The processed expression. 

477 """ 

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

479 

480 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

481 # lobster-trace: SwRequirements.sw_req_rst_record 

482 # lobster-trace: SwRequirements.sw_req_rst_escape 

483 # lobster-trace: SwRequirements.sw_req_rst_string_format 

484 """ 

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

486 a Markdown link. 

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

488 a reStructuredText list of links. 

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

490 

491 Returns: 

492 TrlcAstWalker: The TRLC AST walker. 

493 """ 

494 trlc_ast_walker = TrlcAstWalker() 

495 trlc_ast_walker.add_dispatcher( 

496 Implicit_Null, 

497 None, 

498 self._on_implict_null, 

499 None 

500 ) 

501 trlc_ast_walker.add_dispatcher( 

502 Record_Reference, 

503 None, 

504 self._on_record_reference, 

505 None 

506 ) 

507 trlc_ast_walker.add_dispatcher( 

508 String_Literal, 

509 None, 

510 self._on_string_literal, 

511 None 

512 ) 

513 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher) 

514 

515 return trlc_ast_walker 

516 

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

518 # lobster-trace: SwRequirements.sw_req_rst_string_format 

519 # lobster-trace: SwRequirements.sw_req_rst_render_md 

520 # lobster-trace: SwRequirements.sw_req_rst_render_gfm 

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

522 

523 Args: 

524 package_name (str): The package name. 

525 type_name (str): The type name. 

526 attribute_name (str): The attribute name. 

527 attribute_value (str): The attribute value. 

528 

529 Returns: 

530 str: The rendered attribute value. 

531 """ 

532 result = attribute_value 

533 

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

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

536 

537 # Is it CommonMark Markdown format? 

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

539 # Convert Markdown to reStructuredText. 

540 markdown = Markdown(renderer=Md2RstRenderer) 

541 result = markdown.convert(attribute_value) 

542 

543 # Is it GitHub Flavored Markdown format? 

544 elif self._render_cfg.is_format_gfm(package_name, type_name, attribute_name) is True: 

545 # Convert GitHub Flavored Markdown to reStructuredText. 

546 markdown = Markdown(renderer=Gfm2RstRenderer, extensions=['gfm']) 

547 result = markdown.convert(attribute_value) 

548 

549 # Otherwise escape the text for reStructuredText. 

550 else: 

551 result = self.rst_escape(attribute_value) 

552 

553 return result 

554 

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

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

557 # lobster-trace: SwRequirements.sw_req_rst_record 

558 """ 

559 Process the given record object. 

560 

561 Args: 

562 record (Record_Object): The record object. 

563 level (int): The record level. 

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

565 If None, no translation is applied. 

566  

567 Returns: 

568 Ret: Status 

569 """ 

570 assert self._fd is not None 

571 

572 # The record name will be the admonition. 

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

574 rst_heading = self.rst_create_admonition(record.name, 

575 file_name) 

576 self._fd.write(rst_heading) 

577 

578 self._write_empty_line_on_demand() 

579 

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

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

582 

583 # Build rows for the table. 

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

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

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

587 rows = [] 

588 trlc_ast_walker = self._get_trlc_ast_walker() 

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

590 attribute_name = self._translate_attribute_name(translation, name) 

591 attribute_name = self.rst_escape(attribute_name) 

592 

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

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

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

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

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

598 # its formatting. 

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

600 self._ast_meta_data = { 

601 "package_name": record.n_package.name, 

602 "type_name": record.n_typ.name, 

603 "attribute_name": name 

604 } 

605 walker_result = trlc_ast_walker.walk(value) 

606 

607 attribute_value = "" 

608 if isinstance(walker_result, list): 

609 attribute_value = self.rst_create_list(walker_result, False) 

610 else: 

611 attribute_value = walker_result 

612 

613 rows.append([attribute_name, attribute_value]) 

614 

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

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

617 for row in rows: 

618 for idx, value in enumerate(row): 

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

620 for line in lines: 

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

622 

623 # Write the table head and rows. 

624 rst_table_head = self.rst_create_table_head(column_titles, max_widths) 

625 self._fd.write(rst_table_head) 

626 

627 for row in rows: 

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

629 self._fd.write(rst_table_row) 

630 

631 return Ret.OK 

632 

633 @staticmethod 

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

635 # lobster-trace: SwRequirements.sw_req_rst_escape 

636 """ 

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

638 

639 Args: 

640 text (str): Text to escape 

641 

642 Returns: 

643 str: Escaped text 

644 """ 

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

646 

647 for character in characters: 

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

649 

650 return text 

651 

652 @staticmethod 

653 def rst_create_heading(text: str, 

654 level: int, 

655 file_name: str, 

656 escape: bool = True) -> str: 

657 # lobster-trace: SwRequirements.sw_req_rst_heading 

658 """ 

659 Create a reStructuredText heading with a label. 

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

661 

662 Args: 

663 text (str): Heading text 

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

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

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

667 

668 Returns: 

669 str: reStructuredText heading with a label 

670 """ 

671 result = "" 

672 

673 if 1 <= level <= 7: 

674 text_raw = text 

675 

676 if escape is True: 

677 text_raw = RstConverter.rst_escape(text) 

678 

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

680 

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

682 underline = underline_char * len(text_raw) 

683 

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

685 

686 else: 

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

688 

689 return result 

690 

691 @staticmethod 

692 def rst_create_admonition(text: str, 

693 file_name: str, 

694 escape: bool = True) -> str: 

695 # lobster-trace: SwRequirements.sw_req_rst_admonition 

696 """ 

697 Create a reStructuredText admonition with a label. 

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

699 

700 Args: 

701 text (str): Admonition text 

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

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

704 

705 Returns: 

706 str: reStructuredText admonition with a label 

707 """ 

708 text_raw = text 

709 

710 if escape is True: 

711 text_raw = RstConverter.rst_escape(text) 

712 

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

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

715 

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

717 

718 @staticmethod 

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

720 # lobster-trace: SwRequirements.sw_req_rst_table 

721 """ 

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

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

724 

725 Args: 

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

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

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

729 

730 Returns: 

731 str: Table head 

732 """ 

733 if escape: 

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

735 

736 # Create the top border of the table 

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

738 

739 # Create the title row 

740 table_head += " |" 

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

742 

743 # Create the separator row 

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

745 

746 return table_head 

747 

748 @staticmethod 

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

750 # lobster-trace: SwRequirements.sw_req_rst_table 

751 """ 

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

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

754 Supports multi-line cell values. 

755 

756 Args: 

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

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

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

760 

761 Returns: 

762 str: Table row 

763 """ 

764 if escape: 

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

766 

767 # Split each cell value into lines. 

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

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

770 

771 # Create the row with multi-line support. 

772 table_row = "" 

773 for line_idx in range(max_lines): 

774 table_row += " |" 

775 for col_idx, lines in enumerate(split_values): 

776 if line_idx < len(lines): 

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

778 else: 

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

780 table_row += "|" 

781 table_row += "\n" 

782 

783 # Create the separator row. 

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

785 

786 return table_row + separator_row 

787 

788 @staticmethod 

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

790 # lobster-trace: SwRequirements.sw_req_rst_list 

791 """Create a unordered reStructuredText list. 

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

793 

794 Args: 

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

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

797  

798 Returns: 

799 str: reStructuredText list 

800 """ 

801 list_str = "" 

802 

803 for idx, value_raw in enumerate(list_values): 

804 value = value_raw 

805 

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

807 value = RstConverter.rst_escape(value) 

808 

809 list_str += f"* {value}" 

810 

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

812 if idx < len(list_values) - 1: 

813 list_str += "\n" 

814 

815 return list_str 

816 

817 @staticmethod 

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

819 # lobster-trace: SwRequirements.sw_req_rst_link 

820 """ 

821 Create a reStructuredText cross-reference. 

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

823 There will be no newline appended at the end. 

824 

825 Args: 

826 text (str): Link text 

827 target (str): Cross-reference target 

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

829 

830 Returns: 

831 str: reStructuredText cross-reference 

832 """ 

833 text_raw = text 

834 

835 if escape is True: 

836 text_raw = RstConverter.rst_escape(text) 

837 

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

839 

840 @staticmethod 

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

842 # lobster-trace: SwRequirements.sw_req_rst_image 

843 """ 

844 Create a reStructuredText diagram link. 

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

846 

847 Args: 

848 diagram_file_name (str): Diagram file name 

849 diagram_caption (str): Diagram caption 

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

851 

852 Returns: 

853 str: reStructuredText diagram link 

854 """ 

855 diagram_caption_raw = diagram_caption 

856 

857 if escape is True: 

858 diagram_caption_raw = RstConverter.rst_escape(diagram_caption) 

859 

860 # Allowed are absolute and relative to source paths. 

861 diagram_file_name = os.path.normpath(diagram_file_name) 

862 

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

864 

865 if diagram_caption_raw: 

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

867 

868 return result 

869 

870 @staticmethod 

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

872 # lobster-trace: SwRequirements.sw_req_rst_role 

873 """ 

874 Create role text in reStructuredText. 

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

876 There will be no newline appended at the end. 

877 

878 Args: 

879 text (str): Text 

880 color (str): Role 

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

882 

883 Returns: 

884 str: Text with role 

885 """ 

886 text_raw = text 

887 

888 if escape is True: 

889 text_raw = RstConverter.rst_escape(text) 

890 

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

892 

893# Functions ******************************************************************** 

894 

895# Main *************************************************************************