Coverage for src/pyTRLCConverter/markdown_converter.py: 97%

237 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 10:59 +0000

1"""Converter to Markdown format. 

2 

3 Author: Andreas Merkle (andreas.merkle@newtec.de) 

4""" 

5 

6# pyTRLCConverter - A tool to convert TRLC files to specific formats. 

7# Copyright (c) 2024 - 2025 NewTec GmbH 

8# 

9# This file is part of pyTRLCConverter program. 

10# 

11# The pyTRLCConverter program is free software: you can redistribute it and/or modify it under 

12# the terms of the GNU General Public License as published by the Free Software Foundation, 

13# either version 3 of the License, or (at your option) any later version. 

14# 

15# The pyTRLCConverter program is distributed in the hope that it will be useful, but 

16# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

17# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License along with pyTRLCConverter. 

20# If not, see <https://www.gnu.org/licenses/>. 

21 

22# Imports ********************************************************************** 

23import os 

24from typing import List, Optional 

25from trlc.ast import Implicit_Null, Record_Object, Record_Reference 

26from pyTRLCConverter.base_converter import BaseConverter 

27from pyTRLCConverter.ret import Ret 

28from pyTRLCConverter.trlc_helper import TrlcAstWalker 

29from pyTRLCConverter.logger import log_verbose, log_error 

30 

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

32 

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

34 

35# pylint: disable=too-many-instance-attributes 

36class MarkdownConverter(BaseConverter): 

37 """ 

38 MarkdownConverter provides functionality for converting to a markdown format. 

39 """ 

40 

41 OUTPUT_FILE_NAME_DEFAULT = "output.md" 

42 TOP_LEVEL_DEFAULT = "Specification" 

43 

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. 

49 

50 Args: 

51 args (any): The parsed program arguments. 

52 """ 

53 super().__init__(args) 

54 

55 # The path to the given output folder. 

56 self._out_path = args.out 

57 

58 # The excluded files in normalized form. 

59 self._excluded_files = [] 

60 

61 if args.exclude is not None: 

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

63 

64 # The file descriptor for the output file. 

65 self._fd = None 

66 

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 

70 

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 

75 

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 

80 

81 @staticmethod 

82 def get_subcommand() -> str: 

83 # lobster-trace: SwRequirements.sw_req_markdown 

84 """ 

85 Return subcommand token for this converter. 

86 

87 Returns: 

88 str: Parser subcommand token 

89 """ 

90 return "markdown" 

91 

92 @staticmethod 

93 def get_description() -> str: 

94 # lobster-trace: SwRequirements.sw_req_markdown 

95 """ 

96 Return converter description. 

97 

98 Returns: 

99 str: Converter description 

100 """ 

101 return "Convert into markdown format." 

102 

103 @classmethod 

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

105 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

106 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

107 # lobster-trace: SwRequirements.sw_req_markdown_top_level_default 

108 # lobster-trace: SwRequirements.sw_req_markdown_top_level_custom 

109 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_default 

110 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_custom 

111 """ 

112 Register converter specific argument parser. 

113 

114 Args: 

115 args_parser (any): Argument parser 

116 """ 

117 super().register(args_parser) 

118 

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 ) 

128 

129 BaseConverter._parser.add_argument( 

130 "-n", 

131 "--name", 

132 type=str, 

133 default=MarkdownConverter.OUTPUT_FILE_NAME_DEFAULT, 

134 required=False, 

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

136 f"(default = {MarkdownConverter.OUTPUT_FILE_NAME_DEFAULT}) in " \ 

137 "case a single document is generated." 

138 ) 

139 

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 ) 

148 

149 BaseConverter._parser.add_argument( 

150 "-tl", 

151 "--top-level", 

152 type=str, 

153 default=MarkdownConverter.TOP_LEVEL_DEFAULT, 

154 required=False, 

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

156 f"(default = {MarkdownConverter.TOP_LEVEL_DEFAULT})." 

157 ) 

158 

159 def begin(self) -> Ret: 

160 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

161 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level 

162 """ 

163 Begin the conversion process. 

164 

165 Returns: 

166 Ret: Status 

167 """ 

168 assert self._fd is None 

169 

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

171 result = BaseConverter.begin(self) 

172 

173 if result == Ret.OK: 

174 

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.") 

180 

181 # Set the value for empty attributes. 

182 self._empty_attribute_value = self._args.empty 

183 

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

185 

186 # Single document mode? 

187 if self._args.single_document is True: 

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

189 

190 if self._fd is not None: 

191 self._write_top_level_heading_on_demand() 

192 

193 # All headings will be shifted by one level. 

194 self._base_level = self._base_level + 1 

195 

196 return result 

197 

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

199 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

200 """ 

201 Enter a file. 

202 

203 Args: 

204 file_name (str): File name 

205  

206 Returns: 

207 Ret: Status 

208 """ 

209 result = Ret.OK 

210 

211 # Multiple document mode? 

212 if self._args.single_document is False: 

213 assert self._fd is None 

214 

215 file_name_md = self._file_name_trlc_to_md(file_name) 

216 result = self._generate_out_file(file_name_md) 

217 

218 # The very first written Markdown part shall not have a empty line before. 

219 self._empty_line_required = False 

220 

221 return result 

222 

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

224 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

225 """ 

226 Leave a file. 

227 

228 Args: 

229 file_name (str): File name 

230 

231 Returns: 

232 Ret: Status 

233 """ 

234 

235 # Multiple document mode? 

236 if self._args.single_document is False: 

237 assert self._fd is not None 

238 self._fd.close() 

239 self._fd = None 

240 self._is_top_level_heading_req = True 

241 

242 return Ret.OK 

243 

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

245 # lobster-trace: SwRequirements.sw_req_markdown_section 

246 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

247 """ 

248 Process the given section item. 

249 It will create a Markdown heading with the given section name and level. 

250 

251 Args: 

252 section (str): The section name 

253 level (int): The section indentation level 

254  

255 Returns: 

256 Ret: Status 

257 """ 

258 assert len(section) > 0 

259 assert self._fd is not None 

260 

261 self._write_empty_line_on_demand() 

262 markdown_heading = self.markdown_create_heading(section, self._get_markdown_heading_level(level)) 

263 self._fd.write(markdown_heading) 

264 

265 # If a section heading is written, there is no top level heading required anymore. 

266 self._is_top_level_heading_req = False 

267 

268 return Ret.OK 

269 

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

271 # lobster-trace: SwRequirements.sw_req_markdown_record 

272 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

273 """ 

274 Process the given record object in a generic way. 

275 

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

277 defined for the record type. 

278 

279 Args: 

280 record (Record_Object): The record object. 

281 level (int): The record level. 

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

283 If None, no translation is applied. 

284  

285 Returns: 

286 Ret: Status 

287 """ 

288 assert self._fd is not None 

289 

290 self._write_top_level_heading_on_demand() 

291 self._write_empty_line_on_demand() 

292 

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

294 

295 def finish(self): 

296 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

297 """ 

298 Finish the conversion process. 

299 """ 

300 

301 # Single document mode? 

302 if self._args.single_document is True: 

303 assert self._fd is not None 

304 self._fd.close() 

305 self._fd = None 

306 

307 return Ret.OK 

308 

309 def _write_top_level_heading_on_demand(self) -> None: 

310 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

311 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level 

312 """Write the top level heading if necessary. 

313 """ 

314 if self._is_top_level_heading_req is True: 

315 self._fd.write(MarkdownConverter.markdown_create_heading(self._args.top_level, 1)) 

316 self._empty_line_required = True 

317 self._is_top_level_heading_req = False 

318 

319 def _write_empty_line_on_demand(self) -> None: 

320 # lobster-trace: SwRequirements.sw_req_markdown 

321 """ 

322 Write an empty line if necessary. 

323 

324 For proper Markdown formatting, the first written part shall not have an empty 

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

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

327 line. 

328 """ 

329 if self._empty_line_required is False: 

330 self._empty_line_required = True 

331 else: 

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

333 

334 def _get_markdown_heading_level(self, level: int) -> int: 

335 # lobster-trace: SwRequirements.sw_req_markdown_record 

336 """Get the Markdown heading level from the TRLC object level. 

337 Its mandatory to use this method to calculate the Markdown heading level. 

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

339 

340 Args: 

341 level (int): The TRLC object level. 

342  

343 Returns: 

344 int: Markdown heading level 

345 """ 

346 return self._base_level + level 

347 

348 def _file_name_trlc_to_md(self, file_name_trlc: str) -> str: 

349 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

350 """ 

351 Convert a TRLC file name to a Markdown file name. 

352 

353 Args: 

354 file_name_trlc (str): TRLC file name 

355  

356 Returns: 

357 str: Markdown file name 

358 """ 

359 file_name = os.path.basename(file_name_trlc) 

360 file_name = os.path.splitext(file_name)[0] + ".md" 

361 

362 return file_name 

363 

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

365 # lobster-trace: SwRequirements.sw_req_markdown_out_folder 

366 """ 

367 Generate the output file. 

368 

369 Args: 

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

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

372 

373 Returns: 

374 Ret: Status 

375 """ 

376 result = Ret.OK 

377 file_name_with_path = file_name 

378 

379 # Add path to the output file name. 

380 if 0 < len(self._out_path): 

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

382 

383 try: 

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

385 except IOError as e: 

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

387 result = Ret.ERROR 

388 

389 return result 

390 

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

392 # lobster-trace: SwRequirements.sw_req_markdown_record 

393 """ 

394 Process the given implicit null value. 

395  

396 Returns: 

397 str: The implicit null value 

398 """ 

399 return self.markdown_escape(self._empty_attribute_value) 

400 

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

402 # lobster-trace: SwRequirements.sw_req_markdown_record 

403 """ 

404 Process the given record reference value and return a markdown link. 

405 

406 Args: 

407 record_reference (Record_Reference): The record reference value. 

408  

409 Returns: 

410 str: Markdown link to the record reference. 

411 """ 

412 return self._create_markdown_link_from_record_object_reference(record_reference) 

413 

414 # pylint: disable=line-too-long 

415 def _create_markdown_link_from_record_object_reference(self, record_reference: Record_Reference) -> str: 

416 # lobster-trace: SwRequirements.sw_req_markdown_record 

417 """ 

418 Create a Markdown link from a record reference. 

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

420 

421 Args: 

422 record_reference (Record_Reference): Record reference 

423 

424 Returns: 

425 str: Markdown link 

426 """ 

427 file_name = "" 

428 

429 # Single document mode? 

430 if self._args.single_document is True: 

431 file_name = self._args.name 

432 

433 # Is the link to a excluded file? 

434 for excluded_path in self._excluded_paths: 

435 

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

437 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name) 

438 break 

439 

440 # Multiple document mode 

441 else: 

442 file_name = self._file_name_trlc_to_md(record_reference.target.location.file_name) 

443 

444 record_name = record_reference.target.name 

445 

446 anchor_tag = file_name + "#" + record_name.lower().replace(" ", "-") 

447 

448 return MarkdownConverter.markdown_create_link(str(record_reference.to_python_object()), anchor_tag) 

449 

450 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

451 # lobster-trace: SwRequirements.sw_req_markdown_record 

452 """ 

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

454 a Markdown link. 

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

456 a Markdown list of links. 

457 Otherwise the record object fields attribute values will be written to the Markdown table. 

458 

459 Returns: 

460 TrlcAstWalker: The TRLC AST walker. 

461 """ 

462 trlc_ast_walker = TrlcAstWalker() 

463 trlc_ast_walker.add_dispatcher( 

464 Implicit_Null, 

465 None, 

466 self._on_implict_null, 

467 None 

468 ) 

469 trlc_ast_walker.add_dispatcher( 

470 Record_Reference, 

471 None, 

472 self._on_record_reference, 

473 None 

474 ) 

475 trlc_ast_walker.set_other_dispatcher( 

476 lambda expression: MarkdownConverter.markdown_escape(str(expression.to_python_object())) 

477 ) 

478 

479 return trlc_ast_walker 

480 

481 # pylint: disable=too-many-locals 

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

483 # lobster-trace: SwRequirements.sw_req_markdown_record 

484 """ 

485 Process the given record object. 

486 

487 Args: 

488 record (Record_Object): The record object. 

489 level (int): The record level. 

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

491 If None, no translation is applied. 

492  

493 Returns: 

494 Ret: Status 

495 """ 

496 assert self._fd is not None 

497 

498 # The record name will be the heading. 

499 markdown_heading = self.markdown_create_heading(record.name, self._get_markdown_heading_level(level + 1)) 

500 self._fd.write(markdown_heading) 

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

502 

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

504 # First write the table head. 

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

506 markdown_table_head = self.markdown_create_table_head(column_titles) 

507 self._fd.write(markdown_table_head) 

508 

509 # Walk through the record object fields and write the table rows. 

510 trlc_ast_walker = self._get_trlc_ast_walker() 

511 

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

513 # Translate the attribute name if available. 

514 attribute_name = name 

515 if translation is not None: 

516 if name in translation: 

517 attribute_name = translation[name] 

518 

519 attribute_name = self.markdown_escape(attribute_name) 

520 

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

522 walker_result = trlc_ast_walker.walk(value) 

523 

524 attribute_value = "" 

525 if isinstance(walker_result, list): 

526 attribute_value = self.markdown_create_list(walker_result, True, False) 

527 else: 

528 attribute_value = walker_result 

529 

530 # Write the attribute name and value to the Markdown table as row. 

531 markdown_table_row = self.markdown_append_table_row([attribute_name, attribute_value], False) 

532 self._fd.write(markdown_table_row) 

533 

534 return Ret.OK 

535 

536 @staticmethod 

537 def markdown_escape(text: str) -> str: 

538 # lobster-trace: SwRequirements.sw_req_markdown_escape 

539 """ 

540 Escapes the text to be used in a Markdown document. 

541 

542 Args: 

543 text (str): Text to escape 

544 

545 Returns: 

546 str: Escaped text 

547 """ 

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

549 

550 for character in characters: 

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

552 

553 return text 

554 

555 @staticmethod 

556 def markdown_lf2soft_return(text: str) -> str: 

557 # lobster-trace: SwRequirements.sw_req_markdown_soft_return 

558 """ 

559 A single LF will be converted to backslash + LF. 

560 Use it for paragraphs, but not for headings or tables. 

561 

562 Args: 

563 text (str): Text 

564 Returns: 

565 str: Handled text 

566 """ 

567 return text.replace("\n", "\\\n") 

568 

569 @staticmethod 

570 def markdown_create_heading(text: str, level: int, escape: bool = True) -> str: 

571 # lobster-trace: SwRequirements.sw_req_markdown_heading 

572 """ 

573 Create a Markdown heading. 

574 The text will be automatically escaped for Markdown if necessary. 

575 

576 Args: 

577 text (str): Heading text 

578 level (int): Heading level [1; inf] 

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

580 

581 Returns: 

582 str: Markdown heading 

583 """ 

584 result = "" 

585 

586 if 1 <= level: 

587 text_raw = text 

588 

589 if escape is True: 

590 text_raw = MarkdownConverter.markdown_escape(text) 

591 

592 result = f"{'#' * level} {text_raw}\n" 

593 

594 else: 

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

596 

597 return result 

598 

599 @staticmethod 

600 def markdown_create_table_head(column_titles : List[str], escape: bool = True) -> str: 

601 # lobster-trace: SwRequirements.sw_req_markdown_table 

602 """ 

603 Create the table head for a Markdown table. 

604 The titles will be automatically escaped for Markdown if necessary. 

605 

606 Args: 

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

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

609 

610 Returns: 

611 str: Table head 

612 """ 

613 table_head = "|" 

614 

615 for column_title in column_titles: 

616 column_title_raw = column_title 

617 

618 if escape is True: 

619 column_title_raw = MarkdownConverter.markdown_escape(column_title) 

620 

621 table_head += f" {column_title_raw} |" 

622 

623 table_head += "\n" 

624 

625 table_head += "|" 

626 

627 for column_title in column_titles: 

628 column_title_raw = column_title 

629 

630 if escape is True: 

631 column_title_raw = MarkdownConverter.markdown_escape(column_title) 

632 

633 table_head += " " 

634 

635 for _ in range(len(column_title_raw)): 

636 table_head += "-" 

637 

638 table_head += " |" 

639 

640 table_head += "\n" 

641 

642 return table_head 

643 

644 @staticmethod 

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

646 # lobster-trace: SwRequirements.sw_req_markdown_table 

647 """ 

648 Append a row to a Markdown table. 

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

650 

651 Args: 

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

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

654 

655 Returns: 

656 str: Table row 

657 """ 

658 table_row = "|" 

659 

660 for row_value in row_values: 

661 row_value_raw = row_value 

662 

663 if escape is True: 

664 row_value_raw = MarkdownConverter.markdown_escape(row_value) 

665 

666 # Replace every LF with a HTML <br>. 

667 row_value_raw = row_value_raw.replace("\n", "<br>") 

668 

669 table_row += f" {row_value_raw} |" 

670 

671 table_row += "\n" 

672 

673 return table_row 

674 

675 @staticmethod 

676 def markdown_create_list(list_values: List[str], use_html: bool = False, escape: bool = True) -> str: 

677 # lobster-trace: SwRequirements.sw_req_markdown_list 

678 """Create a unordered Markdown list. 

679 The values will be automatically escaped for Markdown if necessary. 

680 

681 Args: 

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

683 use_html (bool): Use HTML for the list (default: False). 

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

685 Returns: 

686 str: Markdown list 

687 """ 

688 list_str = "" 

689 

690 if use_html is True: 

691 list_str += "<ul>" 

692 

693 for value_raw in list_values: 

694 value = value_raw 

695 

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

697 value = MarkdownConverter.markdown_escape(value) 

698 

699 if use_html is True: 

700 list_str += f"<li>{value}</li>" # No line feed here, because the HTML list is not a Markdown list. 

701 else: 

702 list_str += f"* {value}\n" 

703 

704 if use_html is True: 

705 list_str += "</ul>" # No line feed here, because the HTML list is not a Markdown list. 

706 

707 return list_str 

708 

709 @staticmethod 

710 def markdown_create_link(text: str, url: str, escape: bool = True) -> str: 

711 # lobster-trace: SwRequirements.sw_req_markdown_link 

712 """ 

713 Create a Markdown link. 

714 The text will be automatically escaped for Markdown if necessary. 

715 There will be no newline appended at the end. 

716 

717 Args: 

718 text (str): Link text 

719 url (str): Link URL 

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

721 

722 Returns: 

723 str: Markdown link 

724 """ 

725 text_raw = text 

726 

727 if escape is True: 

728 text_raw = MarkdownConverter.markdown_escape(text) 

729 

730 return f"[{text_raw}]({url})" 

731 

732 @staticmethod 

733 def markdown_create_diagram_link(diagram_file_name: str, diagram_caption: str, escape: bool = True) -> str: 

734 # lobster-trace: SwRequirements.sw_req_markdown_image 

735 """ 

736 Create a Markdown diagram link. 

737 The caption will be automatically escaped for Markdown if necessary. 

738 

739 Args: 

740 diagram_file_name (str): Diagram file name 

741 diagram_caption (str): Diagram caption 

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

743 

744 Returns: 

745 str: Markdown diagram link 

746 """ 

747 diagram_caption_raw = diagram_caption 

748 

749 if escape is True: 

750 diagram_caption_raw = MarkdownConverter.markdown_escape(diagram_caption) 

751 

752 # Allowed are absolute and relative to source paths. 

753 diagram_file_name = os.path.normpath(diagram_file_name) 

754 

755 return f"![{diagram_caption_raw}]({diagram_file_name})\n" 

756 

757 @staticmethod 

758 def markdown_text_color(text: str, color: str, escape: bool = True) -> str: 

759 # lobster-trace: SwRequirements.sw_req_markdown_text_color 

760 """ 

761 Create colored text in Markdown. 

762 The text will be automatically escaped for Markdown if necessary. 

763 There will be no newline appended at the end. 

764 

765 Args: 

766 text (str): Text 

767 color (str): HTML color 

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

769 

770 Returns: 

771 str: Colored text 

772 """ 

773 text_raw = text 

774 

775 if escape is True: 

776 text_raw = MarkdownConverter.markdown_escape(text) 

777 

778 return f"<span style=\"{color}\">{text_raw}</span>" 

779 

780# Functions ******************************************************************** 

781 

782# Main *************************************************************************