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
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-21 12:06 +0000
1"""Converter to reStructuredText format.
3 Author: Gabryel Reyes (gabryel.reyes@newtec.de)
4"""
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/>.
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
33# Variables ********************************************************************
35# Classes **********************************************************************
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"
44 def __init__(self, args: Any) -> None:
45 # lobster-trace: SwRequirements.sw_req_rst
46 """
47 Initializes the converter.
49 Args:
50 args (Any): The parsed program arguments.
51 """
52 super().__init__(args)
54 # The path to the given output folder.
55 self._out_path = args.out
57 # The excluded paths in normalized form.
58 self._excluded_paths = []
60 if args.exclude is not None:
61 self._excluded_paths = [os.path.normpath(path) for path in args.exclude]
63 # The file descriptor for the output file.
64 self._fd = None
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
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
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
79 @staticmethod
80 def get_subcommand() -> str:
81 # lobster-trace: SwRequirements.sw_req_rst
82 """
83 Return subcommand token for this converter.
85 Returns:
86 str: Parser subcommand token
87 """
88 return "rst"
90 @staticmethod
91 def get_description() -> str:
92 # lobster-trace: SwRequirements.sw_req_rst
93 """
94 Return converter description.
96 Returns:
97 str: Converter description
98 """
99 return "Convert into reStructuredText format."
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.
112 Args:
113 args_parser (Any): Argument parser
114 """
115 super().register(args_parser)
117 assert BaseConverter._parser is not None
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 )
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 )
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 )
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 )
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.
165 Returns:
166 Ret: Status
167 """
168 assert self._fd is None
170 # Call the base converter to initialize the common stuff.
171 result = BaseConverter.begin(self)
173 if result == Ret.OK:
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.")
181 # Set the value for empty attributes.
182 self._empty_attribute_value = self._args.empty
184 log_verbose(f"Empty attribute value: {self._empty_attribute_value}")
186 # Single document mode?
187 if self._args.single_document is True:
188 result = self._generate_out_file(self._args.name)
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))
194 # All headings will be shifted by one level.
195 self._base_level = self._base_level + 1
197 return result
199 def enter_file(self, file_name: str) -> Ret:
200 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode
201 """
202 Enter a file.
204 Args:
205 file_name (str): File name
207 Returns:
208 Ret: Status
209 """
210 result = Ret.OK
212 # Multiple document mode?
213 if self._args.single_document is False:
214 assert self._fd is None
216 file_name_rst = self._file_name_trlc_to_rst(file_name)
217 result = self._generate_out_file(file_name_rst)
219 # The very first written reStructuredText part shall not have an empty line before.
220 self._empty_line_required = False
222 return result
224 def leave_file(self, file_name: str) -> Ret:
225 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode
226 """
227 Leave a file.
229 Args:
230 file_name (str): File name
232 Returns:
233 Ret: Status
234 """
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
242 return Ret.OK
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.
250 Args:
251 section (str): The section name
252 level (int): The section indentation level
254 Returns:
255 Ret: Status
256 """
257 assert len(section) > 0
258 assert self._fd is not None
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)
266 return Ret.OK
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.
273 The handler is called by the base converter if no specific handler is
274 defined for the record type.
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.
282 Returns:
283 Ret: Status
284 """
285 assert self._fd is not None
287 self._write_empty_line_on_demand()
289 return self._convert_record_object(record, level, translation)
291 def finish(self):
292 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode
293 """
294 Finish the conversion process.
295 """
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
303 return Ret.OK
305 def _write_empty_line_on_demand(self) -> None:
306 # lobster-trace: SwRequirements.sw_req_rst
307 """
308 Write an empty line if necessary.
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
317 if self._empty_line_required is False:
318 self._empty_line_required = True
319 else:
320 self._fd.write("\n")
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.
329 Args:
330 level (int): The TRLC object level.
332 Returns:
333 int: reStructuredText heading level
334 """
335 return self._base_level + level
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.
342 Args:
343 file_name_trlc (str): TRLC file name
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"
351 return file_name
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.
358 Args:
359 file_name (str): The output file name without path.
360 item_list ([Element]): List of elements.
362 Returns:
363 Ret: Status
364 """
365 result = Ret.OK
366 file_name_with_path = file_name
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)
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
378 return result
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.
385 Returns:
386 str: The implicit null value.
387 """
388 return self.rst_escape(self._empty_attribute_value)
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.
395 Args:
396 record_reference (Record_Reference): The record reference value.
398 Returns:
399 str: reStructuredText link to the record reference.
400 """
401 return self._create_rst_link_from_record_object_reference(record_reference)
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.
409 Args:
410 string_literal (String_Literal): The string literal value.
412 Returns:
413 str: The string literal value.
414 """
415 result = string_literal.to_string()
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", "")
422 result = self._render(package_name, type_name, attribute_name, result)
424 return result
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.
432 Args:
433 record_reference (Record_Reference): Record reference
435 Returns:
436 str: reStructuredText cross-reference
437 """
438 assert record_reference.target is not None
440 file_name = ""
442 # Single document mode?
443 if self._args.single_document is True:
444 file_name = self._args.name
446 # Is the link to a excluded file?
447 for excluded_path in self._excluded_paths:
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
453 # Multiple document mode
454 else:
455 file_name = self._file_name_trlc_to_rst(record_reference.target.location.file_name)
457 record_name = record_reference.target.name
459 # Create a target ID for the record
460 target_id = f"{file_name}-{record_name.lower().replace(' ', '-')}"
462 return RstConverter.rst_create_link(str(record_reference.to_python_object()), target_id)
464 def _other_dispatcher(self, expression: Expression) -> str:
465 """
466 Dispatcher for all other expressions.
468 Args:
469 expression (Expression): The expression to process.
471 Returns:
472 str: The processed expression.
473 """
474 return self.rst_escape(expression.to_string())
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.
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)
511 return trlc_ast_walker
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.
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.
524 Returns:
525 str: The rendered attribute value.
526 """
527 result = attribute_value
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:
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)
538 # Otherwise escape the text for reStructuredText.
539 else:
540 result = self.rst_escape(attribute_value)
542 return result
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.
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.
556 Returns:
557 Ret: Status
558 """
559 assert self._fd is not None
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)
567 self._write_empty_line_on_demand()
569 # The record fields will be written to a table.
570 column_titles = ["Attribute Name", "Attribute Value"]
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)
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)
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
602 rows.append([attribute_name, attribute_value])
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))
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)
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)
620 return Ret.OK
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.
628 Args:
629 text (str): Text to escape
631 Returns:
632 str: Escaped text
633 """
634 characters = ["\\", "`", "*", "_", "{", "}", "[", "]", "<", ">", "(", ")", "#", "+", "-", ".", "!", "|"]
636 for character in characters:
637 text = text.replace(character, "\\" + character)
639 return text
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.
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).
657 Returns:
658 str: reStructuredText heading with a label
659 """
660 result = ""
662 if 1 <= level <= 7:
663 text_raw = text
665 if escape is True:
666 text_raw = RstConverter.rst_escape(text)
668 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}"
670 underline_char = ["=", "#", "~", "^", "\"", "+", "'"][level - 1]
671 underline = underline_char * len(text_raw)
673 result = f".. _{label}:\n\n{text_raw}\n{underline}\n"
675 else:
676 log_error(f"Invalid heading level {level} for {text}.")
678 return result
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.
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).
694 Returns:
695 str: reStructuredText admonition with a label
696 """
697 text_raw = text
699 if escape is True:
700 text_raw = RstConverter.rst_escape(text)
702 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}"
703 admonition_label = f".. admonition:: {text_raw}"
705 return f".. _{label}:\n\n{admonition_label}\n"
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.
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).
719 Returns:
720 str: Table head
721 """
722 if escape:
723 column_titles = [RstConverter.rst_escape(title) for title in column_titles]
725 # Create the top border of the table
726 table_head = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n"
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"
732 # Create the separator row
733 table_head += " +" + "+".join(["=" * (width + 2) for width in max_widths]) + "+\n"
735 return table_head
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.
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).
750 Returns:
751 str: Table row
752 """
753 if escape:
754 row_values = [RstConverter.rst_escape(value) for value in row_values]
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)
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"
772 # Create the separator row.
773 separator_row = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n"
775 return table_row + separator_row
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.
783 Args:
784 list_values (List[str]): List of list values.
785 escape (bool): Escapes every list value (default: True).
787 Returns:
788 str: reStructuredText list
789 """
790 list_str = ""
792 for idx, value_raw in enumerate(list_values):
793 value = value_raw
795 if escape is True: # Escape the value if necessary.
796 value = RstConverter.rst_escape(value)
798 list_str += f"* {value}"
800 # The last list value must not have a newline at the end.
801 if idx < len(list_values) - 1:
802 list_str += "\n"
804 return list_str
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.
814 Args:
815 text (str): Link text
816 target (str): Cross-reference target
817 escape (bool): Escapes text (default: True).
819 Returns:
820 str: reStructuredText cross-reference
821 """
822 text_raw = text
824 if escape is True:
825 text_raw = RstConverter.rst_escape(text)
827 return f":ref:`{text_raw} <{target}>`"
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.
836 Args:
837 diagram_file_name (str): Diagram file name
838 diagram_caption (str): Diagram caption
839 escape (bool): Escapes caption (default: True).
841 Returns:
842 str: reStructuredText diagram link
843 """
844 diagram_caption_raw = diagram_caption
846 if escape is True:
847 diagram_caption_raw = RstConverter.rst_escape(diagram_caption)
849 # Allowed are absolute and relative to source paths.
850 diagram_file_name = os.path.normpath(diagram_file_name)
852 result = f".. figure:: {diagram_file_name}\n :alt: {diagram_caption_raw}\n"
854 if diagram_caption_raw:
855 result += f"\n {diagram_caption_raw}\n"
857 return result
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.
867 Args:
868 text (str): Text
869 color (str): Role
870 escape (bool): Escapes text (default: True).
872 Returns:
873 str: Text with role
874 """
875 text_raw = text
877 if escape is True:
878 text_raw = RstConverter.rst_escape(text)
880 return f":{role}:`{text_raw}`"
882# Functions ********************************************************************
884# Main *************************************************************************