Coverage for src / pyTRLCConverter / markdown_converter.py: 98%
242 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 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 - 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 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 """
428 Process the given string literal value.
430 Args:
431 string_literal (String_Literal): The string literal value.
433 Returns:
434 str: The string literal value.
435 """
436 result = string_literal.to_string()
438 if self._ast_meta_data is not None:
439 package_name = self._ast_meta_data.get("package_name", "")
440 type_name = self._ast_meta_data.get("type_name", "")
441 attribute_name = self._ast_meta_data.get("attribute_name", "")
443 result = self._render(package_name, type_name, attribute_name, result)
445 return result
447 # pylint: disable-next=line-too-long
448 def _create_markdown_link_from_record_object_reference(self, record_reference: Record_Reference) -> str:
449 # lobster-trace: SwRequirements.sw_req_markdown_record
450 """
451 Create a Markdown link from a record reference.
452 It considers the file name, the package name, and the record name.
454 Args:
455 record_reference (Record_Reference): Record reference
457 Returns:
458 str: Markdown link
459 """
460 assert record_reference.target is not None
462 file_name = ""
464 # Single document mode?
465 if self._args.single_document is True:
466 file_name = self._args.name
468 # Is the link to a excluded file?
469 for excluded_path in self._excluded_paths:
471 if os.path.commonpath([excluded_path, record_reference.target.location.file_name]) == excluded_path:
472 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name)
473 break
475 # Multiple document mode
476 else:
477 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name)
479 record_name = record_reference.target.name
481 anchor_tag = file_name + "#" + record_name.lower().replace(" ", "-")
483 return MarkdownConverter.markdown_create_link(str(record_reference.to_python_object()), anchor_tag)
485 def _other_dispatcher(self, expression: Expression) -> str:
486 """
487 Dispatcher for all other expressions.
489 Args:
490 expression (Expression): The expression to process.
492 Returns:
493 str: The processed expression.
494 """
495 return self.markdown_escape(expression.to_string())
497 def _get_trlc_ast_walker(self) -> TrlcAstWalker:
498 # lobster-trace: SwRequirements.sw_req_markdown_record
499 # lobster-trace: SwRequirements.sw_req_markdown_escape
500 # lobster-trace: SwRequirements.sw_req_markdown_string_format
501 """
502 If a record object contains a record reference, the record reference will be converted to
503 a Markdown link.
504 If a record object contains an array of record references, the array will be converted to
505 a Markdown list of links.
506 Otherwise the record object fields attribute values will be written to the Markdown table.
508 Returns:
509 TrlcAstWalker: The TRLC AST walker.
510 """
511 trlc_ast_walker = TrlcAstWalker()
512 trlc_ast_walker.add_dispatcher(
513 Implicit_Null,
514 None,
515 self._on_implict_null,
516 None
517 )
518 trlc_ast_walker.add_dispatcher(
519 Record_Reference,
520 None,
521 self._on_record_reference,
522 None
523 )
524 trlc_ast_walker.add_dispatcher(
525 String_Literal,
526 None,
527 self._on_string_literal,
528 None
529 )
530 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher)
532 return trlc_ast_walker
534 def _render(self, package_name: str, type_name: str, attribute_name: str, attribute_value: str) -> str:
535 # lobster-trace: SwRequirements.sw_req_markdown_string_format
536 # lobster-trace: SwRequirements.sw_req_markdown_render_md
537 """Render the attribute value depened on its format.
539 Args:
540 package_name (str): The package name.
541 type_name (str): The type name.
542 attribute_name (str): The attribute name.
543 attribute_value (str): The attribute value.
545 Returns:
546 str: The rendered attribute value.
547 """
548 result = attribute_value
550 # If the attribute value is not already in Markdown format, it will be escaped.
551 if self._render_cfg.is_format_md(package_name, type_name, attribute_name) is False:
552 result = self.markdown_escape(attribute_value)
553 result = self.markdown_lf2soft_return(result)
555 return result
557 # pylint: disable-next=too-many-locals
558 def _convert_record_object(self, record: Record_Object, level: int, translation: Optional[dict]) -> Ret:
559 # lobster-trace: SwRequirements.sw_req_markdown_record
560 """
561 Process the given record object.
563 Args:
564 record (Record_Object): The record object.
565 level (int): The record level.
566 translation (Optional[dict]): Translation dictionary for the record object.
567 If None, no translation is applied.
569 Returns:
570 Ret: Status
571 """
572 assert self._fd is not None
574 # The record name will be the heading.
575 markdown_heading = self.markdown_create_heading(record.name, self._get_markdown_heading_level(level + 1))
576 self._fd.write(markdown_heading)
577 self._fd.write("\n")
579 # The record fields will be written to a table.
580 # First define the table column titles.
581 table_column_titles = ["Attribute Name", "Attribute Value"]
582 table_rows = []
584 # Walk through the record object fields and write the table rows.
585 trlc_ast_walker = self._get_trlc_ast_walker()
587 for name, value in record.field.items():
588 attribute_name = self._translate_attribute_name(translation, name)
589 attribute_name = self.markdown_escape(attribute_name)
591 # Retrieve the attribute value by processing the field value.
592 # The result will be a string representation of the value.
593 # If the value is an array of record references, the result will be a Markdown list of links.
594 # If the value is a single record reference, the result will be a Markdown link.
595 # If the value is a string literal, the result will be the string literal value that considers
596 # its formatting.
597 # Otherwise the result will be the attribute value in a proper format.
598 self._ast_meta_data = {
599 "package_name": record.n_package.name,
600 "type_name": record.n_typ.name,
601 "attribute_name": name
602 }
603 walker_result = trlc_ast_walker.walk(value)
605 attribute_value = ""
606 if isinstance(walker_result, list):
607 attribute_value = self.markdown_create_list(walker_result, False)
608 else:
609 attribute_value = walker_result
611 # Append the attribute name and value to the table rows.
612 table_rows.append([attribute_name, attribute_value])
614 html_table = self.markdown_create_table(table_column_titles, table_rows)
615 self._fd.write(html_table)
617 return Ret.OK
619 @staticmethod
620 def markdown_escape(text: str) -> str:
621 # lobster-trace: SwRequirements.sw_req_markdown_escape
622 """
623 Escapes the text to be used in a Markdown document.
625 Args:
626 text (str): Text to escape
628 Returns:
629 str: Escaped text
630 """
631 characters = ["\\", "`", "*", "_", "{", "}", "[", "]", "<", ">", "(", ")", "#", "+", "-", ".", "!", "|"]
633 for character in characters:
634 text = text.replace(character, "\\" + character)
636 return text
638 @staticmethod
639 def markdown_lf2soft_return(text: str) -> str:
640 # lobster-trace: SwRequirements.sw_req_markdown_soft_return
641 """
642 A single LF will be converted to backslash + LF.
643 Use it for paragraphs, but not for headings or tables.
645 Args:
646 text (str): Text
647 Returns:
648 str: Handled text
649 """
650 return text.replace("\n", "\\\n")
652 @staticmethod
653 def markdown_create_heading(text: str, level: int, escape: bool = True) -> str:
654 # lobster-trace: SwRequirements.sw_req_markdown_heading
655 """
656 Create a Markdown heading.
657 The text will be automatically escaped for Markdown if necessary.
659 Args:
660 text (str): Heading text
661 level (int): Heading level [1; inf]
662 escape (bool): Escape the text (default: True).
664 Returns:
665 str: Markdown heading
666 """
667 result = ""
669 if 1 <= level:
670 text_raw = text
672 if escape is True:
673 text_raw = MarkdownConverter.markdown_escape(text)
675 result = f"{'#' * level} {text_raw}\n"
677 else:
678 log_error(f"Invalid heading level {level} for {text}.")
680 return result
682 @staticmethod
683 def markdown_create_table(column_titles : List[str], row_values_list: List[List[str]]) -> str:
684 # lobster-trace: SwRequirements.sw_req_markdown_table
685 """
686 Create a complete Markdown table in HTML format to support multi-line cells and
687 other complex content.
689 Args:
690 column_titles (List[str]): List of column titles.
691 row_values_list (List[List[str]]): List of row values.
693 Returns:
694 str: Markdown table
695 """
696 table = "<table>\n"
697 table += "<thead>\n"
698 table += "<tr>\n"
700 for column_title in column_titles:
701 table += f"<th>{column_title}</th>\n"
703 table += "</tr>\n"
704 table += "</thead>\n"
705 table += "<tbody>\n"
707 for row_values in row_values_list:
708 table += "<tr>\n"
710 for cell_value in row_values:
711 # To allow Markdown content inside table cells, a blank line is required
712 # before and after the cell content.
713 # See https://spec.commonmark.org/0.31.2/#html-blocks
714 table += "<td>\n"
715 table += "\n"
716 table += f"{cell_value}\n"
717 table += "\n"
718 table += "</td>\n"
720 table += "</tr>\n"
722 table += "</tbody>\n"
723 table += "</table>\n"
725 return table
727 @staticmethod
728 def markdown_create_list(list_values: List[str], escape: bool = True) -> str:
729 # lobster-trace: SwRequirements.sw_req_markdown_list
730 """Create a unordered Markdown list.
731 The values will be automatically escaped for Markdown if necessary.
733 Args:
734 list_values (List[str]): List of list values.
735 escape (bool): Escapes every list value (default: True).
736 Returns:
737 str: Markdown list
738 """
739 list_str = ""
741 for value_raw in list_values:
742 value = value_raw
744 if escape is True: # Escape the value if necessary.
745 value = MarkdownConverter.markdown_escape(value)
746 list_str += f"- {value}\n"
748 return list_str
750 @staticmethod
751 def markdown_create_link(text: str, url: str, escape: bool = True) -> str:
752 # lobster-trace: SwRequirements.sw_req_markdown_link
753 """
754 Create a Markdown link.
755 The text will be automatically escaped for Markdown if necessary.
756 There will be no newline appended at the end.
758 Args:
759 text (str): Link text
760 url (str): Link URL
761 escape (bool): Escapes text (default: True).
763 Returns:
764 str: Markdown link
765 """
766 text_raw = text
768 if escape is True:
769 text_raw = MarkdownConverter.markdown_escape(text)
771 return f"[{text_raw}]({url})"
773 @staticmethod
774 def markdown_create_diagram_link(diagram_file_name: str, diagram_caption: str, escape: bool = True) -> str:
775 # lobster-trace: SwRequirements.sw_req_markdown_image
776 """
777 Create a Markdown diagram link.
778 The caption will be automatically escaped for Markdown if necessary.
780 Args:
781 diagram_file_name (str): Diagram file name
782 diagram_caption (str): Diagram caption
783 escape (bool): Escapes caption (default: True).
785 Returns:
786 str: Markdown diagram link
787 """
788 diagram_caption_raw = diagram_caption
790 if escape is True:
791 diagram_caption_raw = MarkdownConverter.markdown_escape(diagram_caption)
793 # Allowed are absolute and relative to source paths.
794 diagram_file_name = os.path.normpath(diagram_file_name)
796 return f"\n"
798 @staticmethod
799 def markdown_text_color(text: str, color: str, escape: bool = True) -> str:
800 # lobster-trace: SwRequirements.sw_req_markdown_text_color
801 """
802 Create colored text in Markdown.
803 The text will be automatically escaped for Markdown if necessary.
804 There will be no newline appended at the end.
806 Args:
807 text (str): Text
808 color (str): HTML color
809 escape (bool): Escapes text (default: True).
811 Returns:
812 str: Colored text
813 """
814 text_raw = text
816 if escape is True:
817 text_raw = MarkdownConverter.markdown_escape(text)
819 return f"<span style=\"{color}\">{text_raw}</span>"
821# Functions ********************************************************************
823# Main *************************************************************************