Coverage for src / pyTRLCConverter / markdown_converter.py: 98%
242 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 Markdown format.
3 Author: Andreas Merkle (andreas.merkle@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 trlc.ast import Implicit_Null, Record_Object, Record_Reference, String_Literal, Expression
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
31# Variables ********************************************************************
33# Classes **********************************************************************
35# pylint: disable-next=too-many-instance-attributes
36class MarkdownConverter(BaseConverter):
37 """
38 MarkdownConverter provides functionality for converting to a markdown format.
39 """
41 OUTPUT_FILE_NAME_DEFAULT = "output.md"
42 TOP_LEVEL_DEFAULT = "Specification"
44 def __init__(self, args: Any) -> None:
45 # lobster-trace: SwRequirements.sw_req_no_prj_spec
46 # lobster-trace: SwRequirements.sw_req_markdown
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 Markdown formatting, the first written Markdown 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 # A top level heading is always required to generate a compliant Markdown document.
77 # In single document mode it will always be necessary.
78 # In multiple document mode only if there is no top level section.
79 self._is_top_level_heading_req = True
81 # The AST walker meta data for processing the record object fields.
82 # This will hold the information about the current package, type and attribute being processed.
83 self._ast_meta_data = None
85 @staticmethod
86 def get_subcommand() -> str:
87 # lobster-trace: SwRequirements.sw_req_markdown
88 """
89 Return subcommand token for this converter.
91 Returns:
92 str: Parser subcommand token
93 """
94 return "markdown"
96 @staticmethod
97 def get_description() -> str:
98 # lobster-trace: SwRequirements.sw_req_markdown
99 """
100 Return converter description.
102 Returns:
103 str: Converter description
104 """
105 return "Convert into markdown format."
107 @classmethod
108 def register(cls, args_parser: Any) -> None:
109 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode
110 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode
111 # lobster-trace: SwRequirements.sw_req_markdown_top_level_default
112 # lobster-trace: SwRequirements.sw_req_markdown_top_level_custom
113 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_default
114 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_custom
115 """
116 Register converter specific argument parser.
118 Args:
119 args_parser (Any): Argument parser
120 """
121 super().register(args_parser)
123 assert BaseConverter._parser is not None
125 BaseConverter._parser.add_argument(
126 "-e",
127 "--empty",
128 type=str,
129 default=BaseConverter.EMPTY_ATTRIBUTE_DEFAULT,
130 required=False,
131 help="Every attribute value which is empty will output the string " \
132 f"(default = {BaseConverter.EMPTY_ATTRIBUTE_DEFAULT})."
133 )
135 BaseConverter._parser.add_argument(
136 "-n",
137 "--name",
138 type=str,
139 default=MarkdownConverter.OUTPUT_FILE_NAME_DEFAULT,
140 required=False,
141 help="Name of the generated output file inside the output folder " \
142 f"(default = {MarkdownConverter.OUTPUT_FILE_NAME_DEFAULT}) in " \
143 "case a single document is generated."
144 )
146 BaseConverter._parser.add_argument(
147 "-sd",
148 "--single-document",
149 action="store_true",
150 required=False,
151 default=False,
152 help="Generate a single document instead of multiple files. The default is to generate multiple files."
153 )
155 BaseConverter._parser.add_argument(
156 "-tl",
157 "--top-level",
158 type=str,
159 default=MarkdownConverter.TOP_LEVEL_DEFAULT,
160 required=False,
161 help="Name of the top level heading, required in single document mode " \
162 f"(default = {MarkdownConverter.TOP_LEVEL_DEFAULT})."
163 )
165 def begin(self) -> Ret:
166 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode
167 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level
168 """
169 Begin the conversion process.
171 Returns:
172 Ret: Status
173 """
174 assert self._fd is None
176 # Call the base converter to initialize the common stuff.
177 result = BaseConverter.begin(self)
179 if result == Ret.OK:
181 # Single document mode?
182 if self._args.single_document is True:
183 log_verbose("Single document mode.")
184 else:
185 log_verbose("Multiple document mode.")
187 # Set the value for empty attributes.
188 self._empty_attribute_value = self._args.empty
190 log_verbose(f"Empty attribute value: {self._empty_attribute_value}")
192 # Single document mode?
193 if self._args.single_document is True:
194 result = self._generate_out_file(self._args.name)
196 if self._fd is not None:
197 self._write_top_level_heading_on_demand()
199 # All headings will be shifted by one level.
200 self._base_level = self._base_level + 1
202 return result
204 def enter_file(self, file_name: str) -> Ret:
205 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode
206 """
207 Enter a file.
209 Args:
210 file_name (str): File name
212 Returns:
213 Ret: Status
214 """
215 result = Ret.OK
217 # Multiple document mode?
218 if self._args.single_document is False:
219 assert self._fd is None
221 file_name_md = self._file_name_trlc_to_md(file_name)
222 result = self._generate_out_file(file_name_md)
224 # The very first written Markdown part shall not have a empty line before.
225 self._empty_line_required = False
227 return result
229 def leave_file(self, file_name: str) -> Ret:
230 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode
231 """
232 Leave a file.
234 Args:
235 file_name (str): File name
237 Returns:
238 Ret: Status
239 """
241 # Multiple document mode?
242 if self._args.single_document is False:
243 assert self._fd is not None
244 self._fd.close()
245 self._fd = None
246 self._is_top_level_heading_req = True
248 return Ret.OK
250 def convert_section(self, section: str, level: int) -> Ret:
251 # lobster-trace: SwRequirements.sw_req_markdown_section
252 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level
253 """
254 Process the given section item.
255 It will create a Markdown heading with the given section name and level.
257 Args:
258 section (str): The section name
259 level (int): The section indentation level
261 Returns:
262 Ret: Status
263 """
264 assert len(section) > 0
265 assert self._fd is not None
267 self._write_empty_line_on_demand()
268 markdown_heading = self.markdown_create_heading(section, self._get_markdown_heading_level(level))
269 self._fd.write(markdown_heading)
271 # If a section heading is written, there is no top level heading required anymore.
272 self._is_top_level_heading_req = False
274 return Ret.OK
276 def convert_record_object_generic(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret:
277 # lobster-trace: SwRequirements.sw_req_markdown_record
278 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level
279 """
280 Process the given record object in a generic way.
282 The handler is called by the base converter if no specific handler is
283 defined for the record type.
285 Args:
286 record (Record_Object): The record object.
287 level (int): The record level.
288 translation (Optional[dict]): Translation dictionary for the record object.
289 If None, no translation is applied.
291 Returns:
292 Ret: Status
293 """
294 assert self._fd is not None
296 self._write_top_level_heading_on_demand()
297 self._write_empty_line_on_demand()
299 return self._convert_record_object(record, level, translation)
301 def finish(self):
302 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode
303 """
304 Finish the conversion process.
305 """
307 # Single document mode?
308 if self._args.single_document is True:
309 assert self._fd is not None
310 self._fd.close()
311 self._fd = None
313 return Ret.OK
315 def _write_top_level_heading_on_demand(self) -> None:
316 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level
317 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level
318 """Write the top level heading if necessary.
319 """
320 assert self._fd is not None
322 if self._is_top_level_heading_req is True:
323 self._fd.write(MarkdownConverter.markdown_create_heading(self._args.top_level, 1))
324 self._empty_line_required = True
325 self._is_top_level_heading_req = False
327 def _write_empty_line_on_demand(self) -> None:
328 # lobster-trace: SwRequirements.sw_req_markdown
329 """
330 Write an empty line if necessary.
332 For proper Markdown formatting, the first written part shall not have an empty
333 line before. But all following parts (heading, table, paragraph, image, etc.) shall
334 have an empty line before. And at the document bottom, there shall be just one empty
335 line.
336 """
337 assert self._fd is not None
339 if self._empty_line_required is False:
340 self._empty_line_required = True
341 else:
342 self._fd.write("\n")
344 def _get_markdown_heading_level(self, level: int) -> int:
345 # lobster-trace: SwRequirements.sw_req_markdown_record
346 """Get the Markdown heading level from the TRLC object level.
347 Its mandatory to use this method to calculate the Markdown heading level.
348 Otherwise in single document mode the top level heading will be wrong.
350 Args:
351 level (int): The TRLC object level.
353 Returns:
354 int: Markdown heading level
355 """
356 return self._base_level + level
358 def _file_name_trlc_to_md(self, file_name_trlc: str) -> str:
359 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode
360 """
361 Convert a TRLC file name to a Markdown file name.
363 Args:
364 file_name_trlc (str): TRLC file name
366 Returns:
367 str: Markdown file name
368 """
369 file_name = os.path.basename(file_name_trlc)
370 file_name = os.path.splitext(file_name)[0] + ".md"
372 return file_name
374 def _generate_out_file(self, file_name: str) -> Ret:
375 # lobster-trace: SwRequirements.sw_req_markdown_out_folder
376 """
377 Generate the output file.
379 Args:
380 file_name (str): The output file name without path.
381 item_list ([Element]): List of elements.
383 Returns:
384 Ret: Status
385 """
386 result = Ret.OK
387 file_name_with_path = file_name
389 # Add path to the output file name.
390 if 0 < len(self._out_path):
391 file_name_with_path = os.path.join(self._out_path, file_name)
393 try:
394 self._fd = open(file_name_with_path, "w", encoding="utf-8") #pylint: disable=consider-using-with
395 except IOError as e:
396 log_error(f"Failed to open file {file_name_with_path}: {e}")
397 result = Ret.ERROR
399 return result
401 def _on_implict_null(self, _: Implicit_Null) -> str:
402 # lobster-trace: SwRequirements.sw_req_markdown_record
403 """
404 Process the given implicit null value.
406 Returns:
407 str: The implicit null value.
408 """
409 return self.markdown_escape(self._empty_attribute_value)
411 def _on_record_reference(self, record_reference: Record_Reference) -> str:
412 # lobster-trace: SwRequirements.sw_req_markdown_record
413 """
414 Process the given record reference value and return a markdown link.
416 Args:
417 record_reference (Record_Reference): The record reference value.
419 Returns:
420 str: Markdown link to the record reference.
421 """
422 return self._create_markdown_link_from_record_object_reference(record_reference)
424 def _on_string_literal(self, string_literal: String_Literal) -> str:
425 # lobster-trace: SwRequirements.sw_req_markdown_string_format
426 # lobster-trace: SwRequirements.sw_req_markdown_render_md
427 # lobster-trace: SwRequirements.sw_req_markdown_render_gfm
428 """
429 Process the given string literal value.
431 Args:
432 string_literal (String_Literal): The string literal value.
434 Returns:
435 str: The string literal value.
436 """
437 result = string_literal.to_string()
439 if self._ast_meta_data is not None:
440 package_name = self._ast_meta_data.get("package_name", "")
441 type_name = self._ast_meta_data.get("type_name", "")
442 attribute_name = self._ast_meta_data.get("attribute_name", "")
444 result = self._render(package_name, type_name, attribute_name, result)
446 return result
448 # pylint: disable-next=line-too-long
449 def _create_markdown_link_from_record_object_reference(self, record_reference: Record_Reference) -> str:
450 # lobster-trace: SwRequirements.sw_req_markdown_record
451 """
452 Create a Markdown link from a record reference.
453 It considers the file name, the package name, and the record name.
455 Args:
456 record_reference (Record_Reference): Record reference
458 Returns:
459 str: Markdown link
460 """
461 assert record_reference.target is not None
463 file_name = ""
465 # Single document mode?
466 if self._args.single_document is True:
467 file_name = self._args.name
469 # Is the link to a excluded file?
470 for excluded_path in self._excluded_paths:
472 if os.path.commonpath([excluded_path, record_reference.target.location.file_name]) == excluded_path:
473 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name)
474 break
476 # Multiple document mode
477 else:
478 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name)
480 record_name = record_reference.target.name
482 anchor_tag = file_name + "#" + record_name.lower().replace(" ", "-")
484 return MarkdownConverter.markdown_create_link(str(record_reference.to_python_object()), anchor_tag)
486 def _other_dispatcher(self, expression: Expression) -> str:
487 # lobster-trace: SwRequirements.sw_req_markdown_record
488 # lobster-trace: SwRequirements.sw_req_markdown_escape
489 """
490 Dispatcher for all other expressions.
492 Args:
493 expression (Expression): The expression to process.
495 Returns:
496 str: The processed expression.
497 """
498 return self.markdown_escape(expression.to_string())
500 def _get_trlc_ast_walker(self) -> TrlcAstWalker:
501 # lobster-trace: SwRequirements.sw_req_markdown_record
502 # lobster-trace: SwRequirements.sw_req_markdown_escape
503 # lobster-trace: SwRequirements.sw_req_markdown_string_format
504 """
505 If a record object contains a record reference, the record reference will be converted to
506 a Markdown link.
507 If a record object contains an array of record references, the array will be converted to
508 a Markdown list of links.
509 Otherwise the record object fields attribute values will be written to the Markdown table.
511 Returns:
512 TrlcAstWalker: The TRLC AST walker.
513 """
514 trlc_ast_walker = TrlcAstWalker()
515 trlc_ast_walker.add_dispatcher(
516 Implicit_Null,
517 None,
518 self._on_implict_null,
519 None
520 )
521 trlc_ast_walker.add_dispatcher(
522 Record_Reference,
523 None,
524 self._on_record_reference,
525 None
526 )
527 trlc_ast_walker.add_dispatcher(
528 String_Literal,
529 None,
530 self._on_string_literal,
531 None
532 )
533 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher)
535 return trlc_ast_walker
537 def _render(self, package_name: str, type_name: str, attribute_name: str, attribute_value: str) -> str:
538 # lobster-trace: SwRequirements.sw_req_markdown_string_format
539 # lobster-trace: SwRequirements.sw_req_markdown_render_md
540 # lobster-trace: SwRequirements.sw_req_markdown_render_gfm
541 """Render the attribute value depened on its format.
543 Args:
544 package_name (str): The package name.
545 type_name (str): The type name.
546 attribute_name (str): The attribute name.
547 attribute_value (str): The attribute value.
549 Returns:
550 str: The rendered attribute value.
551 """
552 result = attribute_value
554 # If the attribute value is not already in Markdown format, it will be escaped.
555 if self._render_cfg.is_format_md(package_name, type_name, attribute_name) is False and \
556 self._render_cfg.is_format_gfm(package_name, type_name, attribute_name) is False:
558 result = self.markdown_escape(attribute_value)
559 result = self.markdown_lf2soft_return(result)
561 return result
563 # pylint: disable-next=too-many-locals
564 def _convert_record_object(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret:
565 # lobster-trace: SwRequirements.sw_req_markdown_record
566 """
567 Process the given record object.
569 Args:
570 record (Record_Object): The record object.
571 level (int): The record level.
572 translation (Optional[dict]): Translation dictionary for the record object.
573 If None, no translation is applied.
575 Returns:
576 Ret: Status
577 """
578 assert self._fd is not None
580 # The record name will be the heading.
581 markdown_heading = self.markdown_create_heading(record.name, self._get_markdown_heading_level(level + 1))
582 self._fd.write(markdown_heading)
583 self._fd.write("\n")
585 # The record fields will be written to a table.
586 # First define the table column titles.
587 table_column_titles = ["Attribute Name", "Attribute Value"]
588 table_rows = []
590 # Walk through the record object fields and write the table rows.
591 trlc_ast_walker = self._get_trlc_ast_walker()
593 for name, value in record.field.items():
594 attribute_name = self._translate_attribute_name(translation, name)
595 attribute_name = self.markdown_escape(attribute_name)
597 # Retrieve the attribute value by processing the field value.
598 # The result will be a string representation of the value.
599 # If the value is an array of record references, the result will be a Markdown list of links.
600 # If the value is a single record reference, the result will be a Markdown link.
601 # If the value is a string literal, the result will be the string literal value that considers
602 # its formatting.
603 # Otherwise the result will be the attribute value in a proper format.
604 self._ast_meta_data = {
605 "package_name": record.n_package.name,
606 "type_name": record.n_typ.name,
607 "attribute_name": name
608 }
609 walker_result = trlc_ast_walker.walk(value)
611 attribute_value = ""
612 if isinstance(walker_result, list):
613 attribute_value = self.markdown_create_list(walker_result, False)
614 else:
615 attribute_value = walker_result
617 # Append the attribute name and value to the table rows.
618 table_rows.append([attribute_name, attribute_value])
620 html_table = self.markdown_create_table(table_column_titles, table_rows)
621 self._fd.write(html_table)
623 return Ret.OK
625 @staticmethod
626 def markdown_escape(text: str) -> str:
627 # lobster-trace: SwRequirements.sw_req_markdown_escape
628 """
629 Escapes the text to be used in a Markdown document.
631 Args:
632 text (str): Text to escape
634 Returns:
635 str: Escaped text
636 """
637 characters = ["\\", "`", "*", "_", "{", "}", "[", "]", "<", ">", "(", ")", "#", "+", "-", ".", "!", "|"]
639 for character in characters:
640 text = text.replace(character, "\\" + character)
642 return text
644 @staticmethod
645 def markdown_lf2soft_return(text: str) -> str:
646 # lobster-trace: SwRequirements.sw_req_markdown_soft_return
647 """
648 A single LF will be converted to backslash + LF.
649 Use it for paragraphs, but not for headings or tables.
651 Args:
652 text (str): Text
653 Returns:
654 str: Handled text
655 """
656 return text.replace("\n", "\\\n")
658 @staticmethod
659 def markdown_create_heading(text: str, level: int, escape: bool = True) -> str:
660 # lobster-trace: SwRequirements.sw_req_markdown_heading
661 """
662 Create a Markdown heading.
663 The text will be automatically escaped for Markdown if necessary.
665 Args:
666 text (str): Heading text
667 level (int): Heading level [1; inf]
668 escape (bool): Escape the text (default: True).
670 Returns:
671 str: Markdown heading
672 """
673 result = ""
675 if 1 <= level:
676 text_raw = text
678 if escape is True:
679 text_raw = MarkdownConverter.markdown_escape(text)
681 result = f"{'#' * level} {text_raw}\n"
683 else:
684 log_error(f"Invalid heading level {level} for {text}.")
686 return result
688 @staticmethod
689 def markdown_create_table(column_titles : List[str], row_values_list: List[List[str]]) -> str:
690 # lobster-trace: SwRequirements.sw_req_markdown_table
691 """
692 Create a complete Markdown table in HTML format to support multi-line cells and
693 other complex content.
695 Args:
696 column_titles (List[str]): List of column titles.
697 row_values_list (List[List[str]]): List of row values.
699 Returns:
700 str: Markdown table
701 """
702 table = "<table>\n"
703 table += "<thead>\n"
704 table += "<tr>\n"
706 for column_title in column_titles:
707 table += f"<th>{column_title}</th>\n"
709 table += "</tr>\n"
710 table += "</thead>\n"
711 table += "<tbody>\n"
713 for row_values in row_values_list:
714 table += "<tr>\n"
716 for cell_value in row_values:
717 # To allow Markdown content inside table cells, a blank line is required
718 # before and after the cell content.
719 # See https://spec.commonmark.org/0.31.2/#html-blocks
720 table += "<td>\n"
721 table += "\n"
722 table += f"{cell_value}\n"
723 table += "\n"
724 table += "</td>\n"
726 table += "</tr>\n"
728 table += "</tbody>\n"
729 table += "</table>\n"
731 return table
733 @staticmethod
734 def markdown_create_list(list_values: List[str], escape: bool = True) -> str:
735 # lobster-trace: SwRequirements.sw_req_markdown_list
736 """Create a unordered Markdown list.
737 The values will be automatically escaped for Markdown if necessary.
739 Args:
740 list_values (List[str]): List of list values.
741 escape (bool): Escapes every list value (default: True).
742 Returns:
743 str: Markdown list
744 """
745 list_str = ""
747 for value_raw in list_values:
748 value = value_raw
750 if escape is True: # Escape the value if necessary.
751 value = MarkdownConverter.markdown_escape(value)
752 list_str += f"- {value}\n"
754 return list_str
756 @staticmethod
757 def markdown_create_link(text: str, url: str, escape: bool = True) -> str:
758 # lobster-trace: SwRequirements.sw_req_markdown_link
759 """
760 Create a Markdown link.
761 The text will be automatically escaped for Markdown if necessary.
762 There will be no newline appended at the end.
764 Args:
765 text (str): Link text
766 url (str): Link URL
767 escape (bool): Escapes text (default: True).
769 Returns:
770 str: Markdown link
771 """
772 text_raw = text
774 if escape is True:
775 text_raw = MarkdownConverter.markdown_escape(text)
777 return f"[{text_raw}]({url})"
779 @staticmethod
780 def markdown_create_diagram_link(diagram_file_name: str, diagram_caption: str, escape: bool = True) -> str:
781 # lobster-trace: SwRequirements.sw_req_markdown_image
782 """
783 Create a Markdown diagram link.
784 The caption will be automatically escaped for Markdown if necessary.
786 Args:
787 diagram_file_name (str): Diagram file name
788 diagram_caption (str): Diagram caption
789 escape (bool): Escapes caption (default: True).
791 Returns:
792 str: Markdown diagram link
793 """
794 diagram_caption_raw = diagram_caption
796 if escape is True:
797 diagram_caption_raw = MarkdownConverter.markdown_escape(diagram_caption)
799 # Allowed are absolute and relative to source paths.
800 diagram_file_name = os.path.normpath(diagram_file_name)
802 return f"\n"
804 @staticmethod
805 def markdown_text_color(text: str, color: str, escape: bool = True) -> str:
806 # lobster-trace: SwRequirements.sw_req_markdown_text_color
807 """
808 Create colored text in Markdown.
809 The text will be automatically escaped for Markdown if necessary.
810 There will be no newline appended at the end.
812 Args:
813 text (str): Text
814 color (str): HTML color
815 escape (bool): Escapes text (default: True).
817 Returns:
818 str: Colored text
819 """
820 text_raw = text
822 if escape is True:
823 text_raw = MarkdownConverter.markdown_escape(text)
825 return f"<span style=\"{color}\">{text_raw}</span>"
827# Functions ********************************************************************
829# Main *************************************************************************