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

240 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 10:59 +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 

25from trlc.ast import Implicit_Null, Record_Object, Record_Reference 

26from pyTRLCConverter.base_converter import BaseConverter 

27from pyTRLCConverter.ret import Ret 

28from pyTRLCConverter.trlc_helper import TrlcAstWalker 

29from pyTRLCConverter.logger import log_verbose, log_error 

30 

31# Variables ******************************************************************** 

32 

33# Classes ********************************************************************** 

34 

35class RstConverter(BaseConverter): 

36 """ 

37 RstConverter provides functionality for converting to a reStructuredText format. 

38 """ 

39 OUTPUT_FILE_NAME_DEFAULT = "output.rst" 

40 TOP_LEVEL_DEFAULT = "Specification" 

41 

42 def __init__(self, args: any) -> None: 

43 # lobster-trace: SwRequirements.sw_req_rst 

44 """ 

45 Initializes the converter. 

46 

47 Args: 

48 args (any): The parsed program arguments. 

49 """ 

50 super().__init__(args) 

51 

52 # The path to the given output folder. 

53 self._out_path = args.out 

54 

55 # The excluded paths in normalized form. 

56 self._excluded_paths = [] 

57 

58 if args.exclude is not None: 

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

60 

61 # The file descriptor for the output file. 

62 self._fd = None 

63 

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

65 # on the single/multiple document mode. 

66 self._base_level = 1 

67 

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

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

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

71 self._empty_line_required = False 

72 

73 @staticmethod 

74 def get_subcommand() -> str: 

75 # lobster-trace: SwRequirements.sw_req_rst 

76 """ 

77 Return subcommand token for this converter. 

78 

79 Returns: 

80 str: Parser subcommand token 

81 """ 

82 return "rst" 

83 

84 @staticmethod 

85 def get_description() -> str: 

86 # lobster-trace: SwRequirements.sw_req_rst 

87 """ 

88 Return converter description. 

89 

90 Returns: 

91 str: Converter description 

92 """ 

93 return "Convert into reStructuredText format." 

94 

95 @classmethod 

96 def register(cls, args_parser: any) -> None: 

97 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

98 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

99 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_default 

100 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level_custom 

101 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_default 

102 # lobster-trace: SwRequirements.sw_req_rst_out_file_name_custom 

103 """ 

104 Register converter specific argument parser. 

105 

106 Args: 

107 args_parser (any): Argument parser 

108 """ 

109 super().register(args_parser) 

110 

111 BaseConverter._parser.add_argument( 

112 "-e", 

113 "--empty", 

114 type=str, 

115 default=BaseConverter.EMPTY_ATTRIBUTE_DEFAULT, 

116 required=False, 

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

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

119 ) 

120 

121 BaseConverter._parser.add_argument( 

122 "-n", 

123 "--name", 

124 type=str, 

125 default=RstConverter.OUTPUT_FILE_NAME_DEFAULT, 

126 required=False, 

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

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

129 "case a single document is generated." 

130 ) 

131 

132 BaseConverter._parser.add_argument( 

133 "-sd", 

134 "--single-document", 

135 action="store_true", 

136 required=False, 

137 default=False, 

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

139 ) 

140 

141 BaseConverter._parser.add_argument( 

142 "-tl", 

143 "--top-level", 

144 type=str, 

145 default=RstConverter.TOP_LEVEL_DEFAULT, 

146 required=False, 

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

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

149 ) 

150 

151 def begin(self) -> Ret: 

152 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

153 # lobster-trace: SwRequirements.sw_req_rst_sd_top_level 

154 """ 

155 Begin the conversion process. 

156 

157 Returns: 

158 Ret: Status 

159 """ 

160 assert self._fd is None 

161 

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

163 result = BaseConverter.begin(self) 

164 

165 if result == Ret.OK: 

166 

167 # Single document mode? 

168 if self._args.single_document is True: 

169 log_verbose("Single document mode.") 

170 else: 

171 log_verbose("Multiple document mode.") 

172 

173 # Set the value for empty attributes. 

174 self._empty_attribute_value = self._args.empty 

175 

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

177 

178 # Single document mode? 

179 if self._args.single_document is True: 

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

181 

182 if self._fd is not None: 

183 self._write_empty_line_on_demand() 

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

185 

186 # All headings will be shifted by one level. 

187 self._base_level = self._base_level + 1 

188 

189 return result 

190 

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

192 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

193 """ 

194 Enter a file. 

195 

196 Args: 

197 file_name (str): File name 

198  

199 Returns: 

200 Ret: Status 

201 """ 

202 result = Ret.OK 

203 

204 # Multiple document mode? 

205 if self._args.single_document is False: 

206 assert self._fd is None 

207 

208 file_name_rst = self._file_name_trlc_to_rst(file_name) 

209 result = self._generate_out_file(file_name_rst) 

210 

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

212 self._empty_line_required = False 

213 

214 return result 

215 

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

217 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

218 """ 

219 Leave a file. 

220 

221 Args: 

222 file_name (str): File name 

223 

224 Returns: 

225 Ret: Status 

226 """ 

227 

228 # Multiple document mode? 

229 if self._args.single_document is False: 

230 assert self._fd is not None 

231 self._fd.close() 

232 self._fd = None 

233 

234 return Ret.OK 

235 

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

237 # lobster-trace: SwRequirements.sw_req_rst_section 

238 """ 

239 Process the given section item. 

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

241 

242 Args: 

243 section (str): The section name 

244 level (int): The section indentation level 

245  

246 Returns: 

247 Ret: Status 

248 """ 

249 assert len(section) > 0 

250 assert self._fd is not None 

251 

252 self._write_empty_line_on_demand() 

253 rst_heading = self.rst_create_heading(section, 

254 self._get_rst_heading_level(level), 

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

256 self._fd.write(rst_heading) 

257 

258 return Ret.OK 

259 

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

261 # lobster-trace: SwRequirements.sw_req_rst_record 

262 """ 

263 Process the given record object in a generic way. 

264 

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

266 defined for the record type. 

267 

268 Args: 

269 record (Record_Object): The record object. 

270 level (int): The record level. 

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

272 If None, no translation is applied. 

273  

274 Returns: 

275 Ret: Status 

276 """ 

277 assert self._fd is not None 

278 

279 self._write_empty_line_on_demand() 

280 

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

282 

283 def finish(self): 

284 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode 

285 """ 

286 Finish the conversion process. 

287 """ 

288 

289 # Single document mode? 

290 if self._args.single_document is True: 

291 assert self._fd is not None 

292 self._fd.close() 

293 self._fd = None 

294 

295 return Ret.OK 

296 

297 def _write_empty_line_on_demand(self) -> None: 

298 # lobster-trace: SwRequirements.sw_req_rst 

299 """ 

300 Write an empty line if necessary. 

301 

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

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

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

305 line. 

306 """ 

307 if self._empty_line_required is False: 

308 self._empty_line_required = True 

309 else: 

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

311 

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

313 # lobster-trace: SwRequirements.sw_req_rst_section 

314 """ 

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

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

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

318 

319 Args: 

320 level (int): The TRLC object level. 

321  

322 Returns: 

323 int: reStructuredText heading level 

324 """ 

325 return self._base_level + level 

326 

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

328 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode 

329 """ 

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

331 

332 Args: 

333 file_name_trlc (str): TRLC file name 

334  

335 Returns: 

336 str: reStructuredText file name 

337 """ 

338 file_name = os.path.basename(file_name_trlc) 

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

340 

341 return file_name 

342 

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

344 # lobster-trace: SwRequirements.sw_req_rst_out_folder 

345 """ 

346 Generate the output file. 

347 

348 Args: 

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

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

351 

352 Returns: 

353 Ret: Status 

354 """ 

355 result = Ret.OK 

356 file_name_with_path = file_name 

357 

358 # Add path to the output file name. 

359 if 0 < len(self._out_path): 

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

361 

362 try: 

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

364 except IOError as e: 

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

366 result = Ret.ERROR 

367 

368 return result 

369 

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

371 # lobster-trace: SwRequirements.sw_req_rst_record 

372 """ 

373 Process the given implicit null value. 

374  

375 Returns: 

376 str: The implicit null value 

377 """ 

378 return self.rst_escape(self._empty_attribute_value) 

379 

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

381 # lobster-trace: SwRequirements.sw_req_rst_record 

382 """ 

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

384 

385 Args: 

386 record_reference (Record_Reference): The record reference value. 

387  

388 Returns: 

389 str: reStructuredText link to the record reference. 

390 """ 

391 return self._create_rst_link_from_record_object_reference(record_reference) 

392 

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

394 # lobster-trace: SwRequirements.sw_req_rst_link 

395 """ 

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

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

398 

399 Args: 

400 record_reference (Record_Reference): Record reference 

401 

402 Returns: 

403 str: reStructuredText cross-reference 

404 """ 

405 file_name = "" 

406 

407 # Single document mode? 

408 if self._args.single_document is True: 

409 file_name = self._args.name 

410 

411 # Is the link to a excluded file? 

412 for excluded_path in self._excluded_paths: 

413 

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

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

416 break 

417 

418 # Multiple document mode 

419 else: 

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

421 

422 record_name = record_reference.target.name 

423 

424 # Create a target ID for the record 

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

426 

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

428 

429 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

430 # lobster-trace: SwRequirements.sw_req_rst_record 

431 """ 

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

433 a Markdown link. 

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

435 a reStructuredText list of links. 

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

437 

438 Returns: 

439 TrlcAstWalker: The TRLC AST walker. 

440 """ 

441 trlc_ast_walker = TrlcAstWalker() 

442 trlc_ast_walker.add_dispatcher( 

443 Implicit_Null, 

444 None, 

445 self._on_implict_null, 

446 None 

447 ) 

448 trlc_ast_walker.add_dispatcher( 

449 Record_Reference, 

450 None, 

451 self._on_record_reference, 

452 None 

453 ) 

454 trlc_ast_walker.set_other_dispatcher( 

455 lambda expression: RstConverter.rst_escape(str(expression.to_python_object())) 

456 ) 

457 

458 return trlc_ast_walker 

459 

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

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

462 # lobster-trace: SwRequirements.sw_req_rst_record 

463 """ 

464 Process the given record object. 

465 

466 Args: 

467 record (Record_Object): The record object. 

468 level (int): The record level. 

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

470 If None, no translation is applied. 

471  

472 Returns: 

473 Ret: Status 

474 """ 

475 assert self._fd is not None 

476 

477 # The record name will be the admonition. 

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

479 rst_heading = self.rst_create_admonition(record.name, 

480 file_name) 

481 self._fd.write(rst_heading) 

482 

483 self._write_empty_line_on_demand() 

484 

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

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

487 

488 # Build rows for the table. 

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

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

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

492 rows = [] 

493 trlc_ast_walker = self._get_trlc_ast_walker() 

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

495 attribute_name = name 

496 if translation is not None and name in translation: 

497 attribute_name = translation[name] 

498 attribute_name = self.rst_escape(attribute_name) 

499 

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

501 walker_result = trlc_ast_walker.walk(value) 

502 

503 attribute_value = "" 

504 if isinstance(walker_result, list): 

505 attribute_value = self.rst_create_list(walker_result, False) 

506 else: 

507 attribute_value = walker_result 

508 

509 rows.append([attribute_name, attribute_value]) 

510 

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

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

513 for row in rows: 

514 for idx, value in enumerate(row): 

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

516 for line in lines: 

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

518 

519 # Write the table head and rows. 

520 rst_table_head = self.rst_create_table_head(column_titles, max_widths) 

521 self._fd.write(rst_table_head) 

522 

523 for row in rows: 

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

525 self._fd.write(rst_table_row) 

526 

527 return Ret.OK 

528 

529 @staticmethod 

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

531 # lobster-trace: SwRequirements.sw_req_rst_escape 

532 """ 

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

534 

535 Args: 

536 text (str): Text to escape 

537 

538 Returns: 

539 str: Escaped text 

540 """ 

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

542 

543 for character in characters: 

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

545 

546 return text 

547 

548 @staticmethod 

549 def rst_create_heading(text: str, 

550 level: int, 

551 file_name: str, 

552 escape: bool = True) -> str: 

553 # lobster-trace: SwRequirements.sw_req_rst_heading 

554 """ 

555 Create a reStructuredText heading with a label. 

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

557 

558 Args: 

559 text (str): Heading text 

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

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

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

563 

564 Returns: 

565 str: reStructuredText heading with a label 

566 """ 

567 result = "" 

568 

569 if 1 <= level <= 7: 

570 text_raw = text 

571 

572 if escape is True: 

573 text_raw = RstConverter.rst_escape(text) 

574 

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

576 

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

578 underline = underline_char * len(text_raw) 

579 

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

581 

582 else: 

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

584 

585 return result 

586 

587 @staticmethod 

588 def rst_create_admonition(text: str, 

589 file_name: str, 

590 escape: bool = True) -> str: 

591 # lobster-trace: SwRequirements.sw_req_rst_admonition 

592 """ 

593 Create a reStructuredText admonition with a label. 

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

595 

596 Args: 

597 text (str): Admonition text 

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

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

600 

601 Returns: 

602 str: reStructuredText admonition with a label 

603 """ 

604 text_raw = text 

605 

606 if escape is True: 

607 text_raw = RstConverter.rst_escape(text) 

608 

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

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

611 

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

613 

614 @staticmethod 

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

616 # lobster-trace: SwRequirements.sw_req_rst_table 

617 """ 

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

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

620 

621 Args: 

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

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

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

625 

626 Returns: 

627 str: Table head 

628 """ 

629 if escape: 

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

631 

632 # Create the top border of the table 

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

634 

635 # Create the title row 

636 table_head += " |" 

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

638 

639 # Create the separator row 

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

641 

642 return table_head 

643 

644 @staticmethod 

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

646 # lobster-trace: SwRequirements.sw_req_rst_table 

647 """ 

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

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

650 Supports multi-line cell values. 

651 

652 Args: 

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

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

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

656 

657 Returns: 

658 str: Table row 

659 """ 

660 if escape: 

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

662 

663 # Split each cell value into lines. 

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

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

666 

667 # Create the row with multi-line support. 

668 table_row = "" 

669 for line_idx in range(max_lines): 

670 table_row += " |" 

671 for col_idx, lines in enumerate(split_values): 

672 if line_idx < len(lines): 

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

674 else: 

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

676 table_row += "|" 

677 table_row += "\n" 

678 

679 # Create the separator row. 

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

681 

682 return table_row + separator_row 

683 

684 @staticmethod 

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

686 # lobster-trace: SwRequirements.sw_req_rst_list 

687 """Create a unordered reStructuredText list. 

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

689 

690 Args: 

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

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

693  

694 Returns: 

695 str: reStructuredText list 

696 """ 

697 list_str = "" 

698 

699 for idx, value_raw in enumerate(list_values): 

700 value = value_raw 

701 

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

703 value = RstConverter.rst_escape(value) 

704 

705 list_str += f"* {value}" 

706 

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

708 if idx < len(list_values) - 1: 

709 list_str += "\n" 

710 

711 return list_str 

712 

713 @staticmethod 

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

715 # lobster-trace: SwRequirements.sw_req_rst_link 

716 """ 

717 Create a reStructuredText cross-reference. 

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

719 There will be no newline appended at the end. 

720 

721 Args: 

722 text (str): Link text 

723 target (str): Cross-reference target 

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

725 

726 Returns: 

727 str: reStructuredText cross-reference 

728 """ 

729 text_raw = text 

730 

731 if escape is True: 

732 text_raw = RstConverter.rst_escape(text) 

733 

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

735 

736 @staticmethod 

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

738 # lobster-trace: SwRequirements.sw_req_rst_image 

739 """ 

740 Create a reStructuredText diagram link. 

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

742 

743 Args: 

744 diagram_file_name (str): Diagram file name 

745 diagram_caption (str): Diagram caption 

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

747 

748 Returns: 

749 str: reStructuredText diagram link 

750 """ 

751 diagram_caption_raw = diagram_caption 

752 

753 if escape is True: 

754 diagram_caption_raw = RstConverter.rst_escape(diagram_caption) 

755 

756 # Allowed are absolute and relative to source paths. 

757 diagram_file_name = os.path.normpath(diagram_file_name) 

758 

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

760 

761 if diagram_caption_raw: 

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

763 

764 return result 

765 

766 @staticmethod 

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

768 # lobster-trace: SwRequirements.sw_req_rst_role 

769 """ 

770 Create role text in reStructuredText. 

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

772 There will be no newline appended at the end. 

773 

774 Args: 

775 text (str): Text 

776 color (str): Role 

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

778 

779 Returns: 

780 str: Text with role 

781 """ 

782 text_raw = text 

783 

784 if escape is True: 

785 text_raw = RstConverter.rst_escape(text) 

786 

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

788 

789# Functions ******************************************************************** 

790 

791# Main *************************************************************************