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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 12:20 +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 - 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/>.
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
34# Variables ********************************************************************
36# Classes **********************************************************************
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"
45 def __init__(self, args: Any) -> None:
46 # lobster-trace: SwRequirements.sw_req_rst
47 """
48 Initializes the converter.
50 Args:
51 args (Any): The parsed program arguments.
52 """
53 super().__init__(args)
55 # The path to the given output folder.
56 self._out_path = args.out
58 # The excluded paths in normalized form.
59 self._excluded_paths = []
61 if args.exclude is not None:
62 self._excluded_paths = [os.path.normpath(path) for path in args.exclude]
64 # The file descriptor for the output file.
65 self._fd = None
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
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
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
80 @staticmethod
81 def get_subcommand() -> str:
82 # lobster-trace: SwRequirements.sw_req_rst
83 """
84 Return subcommand token for this converter.
86 Returns:
87 str: Parser subcommand token
88 """
89 return "rst"
91 @staticmethod
92 def get_description() -> str:
93 # lobster-trace: SwRequirements.sw_req_rst
94 """
95 Return converter description.
97 Returns:
98 str: Converter description
99 """
100 return "Convert into reStructuredText format."
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.
113 Args:
114 args_parser (Any): Argument parser
115 """
116 super().register(args_parser)
118 assert BaseConverter._parser is not None
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 )
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 )
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 )
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 )
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.
166 Returns:
167 Ret: Status
168 """
169 assert self._fd is None
171 # Call the base converter to initialize the common stuff.
172 result = BaseConverter.begin(self)
174 if result == Ret.OK:
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.")
182 # Set the value for empty attributes.
183 self._empty_attribute_value = self._args.empty
185 log_verbose(f"Empty attribute value: {self._empty_attribute_value}")
187 # Single document mode?
188 if self._args.single_document is True:
189 result = self._generate_out_file(self._args.name)
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))
195 # All headings will be shifted by one level.
196 self._base_level = self._base_level + 1
198 return result
200 def enter_file(self, file_name: str) -> Ret:
201 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode
202 """
203 Enter a file.
205 Args:
206 file_name (str): File name
208 Returns:
209 Ret: Status
210 """
211 result = Ret.OK
213 # Multiple document mode?
214 if self._args.single_document is False:
215 assert self._fd is None
217 file_name_rst = self._file_name_trlc_to_rst(file_name)
218 result = self._generate_out_file(file_name_rst)
220 # The very first written reStructuredText part shall not have an empty line before.
221 self._empty_line_required = False
223 return result
225 def leave_file(self, file_name: str) -> Ret:
226 # lobster-trace: SwRequirements.sw_req_rst_multiple_doc_mode
227 """
228 Leave a file.
230 Args:
231 file_name (str): File name
233 Returns:
234 Ret: Status
235 """
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
243 return Ret.OK
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.
251 Args:
252 section (str): The section name
253 level (int): The section indentation level
255 Returns:
256 Ret: Status
257 """
258 assert len(section) > 0
259 assert self._fd is not None
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)
267 return Ret.OK
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.
274 The handler is called by the base converter if no specific handler is
275 defined for the record type.
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.
283 Returns:
284 Ret: Status
285 """
286 assert self._fd is not None
288 self._write_empty_line_on_demand()
290 return self._convert_record_object(record, level, translation)
292 def finish(self):
293 # lobster-trace: SwRequirements.sw_req_rst_single_doc_mode
294 """
295 Finish the conversion process.
296 """
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
304 return Ret.OK
306 def _write_empty_line_on_demand(self) -> None:
307 # lobster-trace: SwRequirements.sw_req_rst
308 """
309 Write an empty line if necessary.
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
318 if self._empty_line_required is False:
319 self._empty_line_required = True
320 else:
321 self._fd.write("\n")
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.
330 Args:
331 level (int): The TRLC object level.
333 Returns:
334 int: reStructuredText heading level
335 """
336 return self._base_level + level
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.
343 Args:
344 file_name_trlc (str): TRLC file name
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"
352 return file_name
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.
359 Args:
360 file_name (str): The output file name without path.
361 item_list ([Element]): List of elements.
363 Returns:
364 Ret: Status
365 """
366 result = Ret.OK
367 file_name_with_path = file_name
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)
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
379 return result
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.
386 Returns:
387 str: The implicit null value.
388 """
389 return self.rst_escape(self._empty_attribute_value)
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.
396 Args:
397 record_reference (Record_Reference): The record reference value.
399 Returns:
400 str: reStructuredText link to the record reference.
401 """
402 return self._create_rst_link_from_record_object_reference(record_reference)
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.
411 Args:
412 string_literal (String_Literal): The string literal value.
414 Returns:
415 str: The string literal value.
416 """
417 result = string_literal.to_string()
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", "")
424 result = self._render(package_name, type_name, attribute_name, result)
426 return result
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.
434 Args:
435 record_reference (Record_Reference): Record reference
437 Returns:
438 str: reStructuredText cross-reference
439 """
440 assert record_reference.target is not None
442 file_name = ""
444 # Single document mode?
445 if self._args.single_document is True:
446 file_name = self._args.name
448 # Is the link to a excluded file?
449 for excluded_path in self._excluded_paths:
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
455 # Multiple document mode
456 else:
457 file_name = self._file_name_trlc_to_rst(record_reference.target.location.file_name)
459 record_name = record_reference.target.name
461 # Create a target ID for the record
462 target_id = f"{file_name}-{record_name.lower().replace(' ', '-')}"
464 return RstConverter.rst_create_link(str(record_reference.to_python_object()), target_id)
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.
472 Args:
473 expression (Expression): The expression to process.
475 Returns:
476 str: The processed expression.
477 """
478 return self.rst_escape(expression.to_string())
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.
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)
515 return trlc_ast_walker
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.
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.
529 Returns:
530 str: The rendered attribute value.
531 """
532 result = attribute_value
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:
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)
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)
549 # Otherwise escape the text for reStructuredText.
550 else:
551 result = self.rst_escape(attribute_value)
553 return result
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.
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.
567 Returns:
568 Ret: Status
569 """
570 assert self._fd is not None
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)
578 self._write_empty_line_on_demand()
580 # The record fields will be written to a table.
581 column_titles = ["Attribute Name", "Attribute Value"]
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)
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)
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
613 rows.append([attribute_name, attribute_value])
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))
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)
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)
631 return Ret.OK
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.
639 Args:
640 text (str): Text to escape
642 Returns:
643 str: Escaped text
644 """
645 characters = ["\\", "`", "*", "_", "{", "}", "[", "]", "<", ">", "(", ")", "#", "+", "-", ".", "!", "|"]
647 for character in characters:
648 text = text.replace(character, "\\" + character)
650 return text
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.
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).
668 Returns:
669 str: reStructuredText heading with a label
670 """
671 result = ""
673 if 1 <= level <= 7:
674 text_raw = text
676 if escape is True:
677 text_raw = RstConverter.rst_escape(text)
679 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}"
681 underline_char = ["=", "#", "~", "^", "\"", "+", "'"][level - 1]
682 underline = underline_char * len(text_raw)
684 result = f".. _{label}:\n\n{text_raw}\n{underline}\n"
686 else:
687 log_error(f"Invalid heading level {level} for {text}.")
689 return result
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.
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).
705 Returns:
706 str: reStructuredText admonition with a label
707 """
708 text_raw = text
710 if escape is True:
711 text_raw = RstConverter.rst_escape(text)
713 label = f"{file_name}-{text_raw.lower().replace(' ', '-')}"
714 admonition_label = f".. admonition:: {text_raw}"
716 return f".. _{label}:\n\n{admonition_label}\n"
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.
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).
730 Returns:
731 str: Table head
732 """
733 if escape:
734 column_titles = [RstConverter.rst_escape(title) for title in column_titles]
736 # Create the top border of the table
737 table_head = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n"
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"
743 # Create the separator row
744 table_head += " +" + "+".join(["=" * (width + 2) for width in max_widths]) + "+\n"
746 return table_head
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.
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).
761 Returns:
762 str: Table row
763 """
764 if escape:
765 row_values = [RstConverter.rst_escape(value) for value in row_values]
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)
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"
783 # Create the separator row.
784 separator_row = " +" + "+".join(["-" * (width + 2) for width in max_widths]) + "+\n"
786 return table_row + separator_row
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.
794 Args:
795 list_values (List[str]): List of list values.
796 escape (bool): Escapes every list value (default: True).
798 Returns:
799 str: reStructuredText list
800 """
801 list_str = ""
803 for idx, value_raw in enumerate(list_values):
804 value = value_raw
806 if escape is True: # Escape the value if necessary.
807 value = RstConverter.rst_escape(value)
809 list_str += f"* {value}"
811 # The last list value must not have a newline at the end.
812 if idx < len(list_values) - 1:
813 list_str += "\n"
815 return list_str
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.
825 Args:
826 text (str): Link text
827 target (str): Cross-reference target
828 escape (bool): Escapes text (default: True).
830 Returns:
831 str: reStructuredText cross-reference
832 """
833 text_raw = text
835 if escape is True:
836 text_raw = RstConverter.rst_escape(text)
838 return f":ref:`{text_raw} <{target}>`"
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.
847 Args:
848 diagram_file_name (str): Diagram file name
849 diagram_caption (str): Diagram caption
850 escape (bool): Escapes caption (default: True).
852 Returns:
853 str: reStructuredText diagram link
854 """
855 diagram_caption_raw = diagram_caption
857 if escape is True:
858 diagram_caption_raw = RstConverter.rst_escape(diagram_caption)
860 # Allowed are absolute and relative to source paths.
861 diagram_file_name = os.path.normpath(diagram_file_name)
863 result = f".. figure:: {diagram_file_name}\n :alt: {diagram_caption_raw}\n"
865 if diagram_caption_raw:
866 result += f"\n {diagram_caption_raw}\n"
868 return result
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.
878 Args:
879 text (str): Text
880 color (str): Role
881 escape (bool): Escapes text (default: True).
883 Returns:
884 str: Text with role
885 """
886 text_raw = text
888 if escape is True:
889 text_raw = RstConverter.rst_escape(text)
891 return f":{role}:`{text_raw}`"
893# Functions ********************************************************************
895# Main *************************************************************************