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

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, 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 

30 

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

32 

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

34 

35# pylint: disable-next=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 paths in normalized form. 

59 self._excluded_paths = [] 

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 # 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 

84 

85 @staticmethod 

86 def get_subcommand() -> str: 

87 # lobster-trace: SwRequirements.sw_req_markdown 

88 """ 

89 Return subcommand token for this converter. 

90 

91 Returns: 

92 str: Parser subcommand token 

93 """ 

94 return "markdown" 

95 

96 @staticmethod 

97 def get_description() -> str: 

98 # lobster-trace: SwRequirements.sw_req_markdown 

99 """ 

100 Return converter description. 

101 

102 Returns: 

103 str: Converter description 

104 """ 

105 return "Convert into markdown format." 

106 

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. 

117 

118 Args: 

119 args_parser (Any): Argument parser 

120 """ 

121 super().register(args_parser) 

122 

123 assert BaseConverter._parser is not None 

124 

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 ) 

134 

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 ) 

145 

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 ) 

154 

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 ) 

164 

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. 

170 

171 Returns: 

172 Ret: Status 

173 """ 

174 assert self._fd is None 

175 

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

177 result = BaseConverter.begin(self) 

178 

179 if result == Ret.OK: 

180 

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

186 

187 # Set the value for empty attributes. 

188 self._empty_attribute_value = self._args.empty 

189 

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

191 

192 # Single document mode? 

193 if self._args.single_document is True: 

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

195 

196 if self._fd is not None: 

197 self._write_top_level_heading_on_demand() 

198 

199 # All headings will be shifted by one level. 

200 self._base_level = self._base_level + 1 

201 

202 return result 

203 

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

205 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

206 """ 

207 Enter a file. 

208 

209 Args: 

210 file_name (str): File name 

211  

212 Returns: 

213 Ret: Status 

214 """ 

215 result = Ret.OK 

216 

217 # Multiple document mode? 

218 if self._args.single_document is False: 

219 assert self._fd is None 

220 

221 file_name_md = self._file_name_trlc_to_md(file_name) 

222 result = self._generate_out_file(file_name_md) 

223 

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

225 self._empty_line_required = False 

226 

227 return result 

228 

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

230 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

231 """ 

232 Leave a file. 

233 

234 Args: 

235 file_name (str): File name 

236 

237 Returns: 

238 Ret: Status 

239 """ 

240 

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 

247 

248 return Ret.OK 

249 

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. 

256 

257 Args: 

258 section (str): The section name 

259 level (int): The section indentation level 

260  

261 Returns: 

262 Ret: Status 

263 """ 

264 assert len(section) > 0 

265 assert self._fd is not None 

266 

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) 

270 

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

272 self._is_top_level_heading_req = False 

273 

274 return Ret.OK 

275 

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. 

281 

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

283 defined for the record type. 

284 

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. 

290  

291 Returns: 

292 Ret: Status 

293 """ 

294 assert self._fd is not None 

295 

296 self._write_top_level_heading_on_demand() 

297 self._write_empty_line_on_demand() 

298 

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

300 

301 def finish(self): 

302 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

303 """ 

304 Finish the conversion process. 

305 """ 

306 

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 

312 

313 return Ret.OK 

314 

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 

321 

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 

326 

327 def _write_empty_line_on_demand(self) -> None: 

328 # lobster-trace: SwRequirements.sw_req_markdown 

329 """ 

330 Write an empty line if necessary. 

331 

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 

338 

339 if self._empty_line_required is False: 

340 self._empty_line_required = True 

341 else: 

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

343 

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. 

349 

350 Args: 

351 level (int): The TRLC object level. 

352  

353 Returns: 

354 int: Markdown heading level 

355 """ 

356 return self._base_level + level 

357 

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. 

362 

363 Args: 

364 file_name_trlc (str): TRLC file name 

365  

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" 

371 

372 return file_name 

373 

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. 

378 

379 Args: 

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

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

382 

383 Returns: 

384 Ret: Status 

385 """ 

386 result = Ret.OK 

387 file_name_with_path = file_name 

388 

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) 

392 

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 

398 

399 return result 

400 

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. 

405  

406 Returns: 

407 str: The implicit null value. 

408 """ 

409 return self.markdown_escape(self._empty_attribute_value) 

410 

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. 

415 

416 Args: 

417 record_reference (Record_Reference): The record reference value. 

418  

419 Returns: 

420 str: Markdown link to the record reference. 

421 """ 

422 return self._create_markdown_link_from_record_object_reference(record_reference) 

423 

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. 

429 

430 Args: 

431 string_literal (String_Literal): The string literal value. 

432  

433 Returns: 

434 str: The string literal value. 

435 """ 

436 result = string_literal.to_string() 

437 

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

442 

443 result = self._render(package_name, type_name, attribute_name, result) 

444 

445 return result 

446 

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. 

453 

454 Args: 

455 record_reference (Record_Reference): Record reference 

456 

457 Returns: 

458 str: Markdown link 

459 """ 

460 assert record_reference.target is not None 

461 

462 file_name = "" 

463 

464 # Single document mode? 

465 if self._args.single_document is True: 

466 file_name = self._args.name 

467 

468 # Is the link to a excluded file? 

469 for excluded_path in self._excluded_paths: 

470 

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 

474 

475 # Multiple document mode 

476 else: 

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

478 

479 record_name = record_reference.target.name 

480 

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

482 

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

484 

485 def _other_dispatcher(self, expression: Expression) -> str: 

486 """ 

487 Dispatcher for all other expressions. 

488 

489 Args: 

490 expression (Expression): The expression to process. 

491 

492 Returns: 

493 str: The processed expression. 

494 """ 

495 return self.markdown_escape(expression.to_string()) 

496 

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. 

507 

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) 

531 

532 return trlc_ast_walker 

533 

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. 

538 

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. 

544 

545 Returns: 

546 str: The rendered attribute value. 

547 """ 

548 result = attribute_value 

549 

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) 

554 

555 return result 

556 

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. 

562 

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. 

568  

569 Returns: 

570 Ret: Status 

571 """ 

572 assert self._fd is not None 

573 

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

578 

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 = [] 

583 

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

585 trlc_ast_walker = self._get_trlc_ast_walker() 

586 

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) 

590 

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) 

604 

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 

610 

611 # Append the attribute name and value to the table rows. 

612 table_rows.append([attribute_name, attribute_value]) 

613 

614 html_table = self.markdown_create_table(table_column_titles, table_rows) 

615 self._fd.write(html_table) 

616 

617 return Ret.OK 

618 

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. 

624 

625 Args: 

626 text (str): Text to escape 

627 

628 Returns: 

629 str: Escaped text 

630 """ 

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

632 

633 for character in characters: 

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

635 

636 return text 

637 

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. 

644 

645 Args: 

646 text (str): Text 

647 Returns: 

648 str: Handled text 

649 """ 

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

651 

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. 

658 

659 Args: 

660 text (str): Heading text 

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

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

663 

664 Returns: 

665 str: Markdown heading 

666 """ 

667 result = "" 

668 

669 if 1 <= level: 

670 text_raw = text 

671 

672 if escape is True: 

673 text_raw = MarkdownConverter.markdown_escape(text) 

674 

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

676 

677 else: 

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

679 

680 return result 

681 

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. 

688 

689 Args: 

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

691 row_values_list (List[List[str]]): List of row values. 

692 

693 Returns: 

694 str: Markdown table 

695 """ 

696 table = "<table>\n" 

697 table += "<thead>\n" 

698 table += "<tr>\n" 

699 

700 for column_title in column_titles: 

701 table += f"<th>{column_title}</th>\n" 

702 

703 table += "</tr>\n" 

704 table += "</thead>\n" 

705 table += "<tbody>\n" 

706 

707 for row_values in row_values_list: 

708 table += "<tr>\n" 

709 

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" 

719 

720 table += "</tr>\n" 

721 

722 table += "</tbody>\n" 

723 table += "</table>\n" 

724 

725 return table 

726 

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. 

732 

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 = "" 

740 

741 for value_raw in list_values: 

742 value = value_raw 

743 

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

745 value = MarkdownConverter.markdown_escape(value) 

746 list_str += f"- {value}\n" 

747 

748 return list_str 

749 

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. 

757 

758 Args: 

759 text (str): Link text 

760 url (str): Link URL 

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

762 

763 Returns: 

764 str: Markdown link 

765 """ 

766 text_raw = text 

767 

768 if escape is True: 

769 text_raw = MarkdownConverter.markdown_escape(text) 

770 

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

772 

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. 

779 

780 Args: 

781 diagram_file_name (str): Diagram file name 

782 diagram_caption (str): Diagram caption 

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

784 

785 Returns: 

786 str: Markdown diagram link 

787 """ 

788 diagram_caption_raw = diagram_caption 

789 

790 if escape is True: 

791 diagram_caption_raw = MarkdownConverter.markdown_escape(diagram_caption) 

792 

793 # Allowed are absolute and relative to source paths. 

794 diagram_file_name = os.path.normpath(diagram_file_name) 

795 

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

797 

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. 

805 

806 Args: 

807 text (str): Text 

808 color (str): HTML color 

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

810 

811 Returns: 

812 str: Colored text 

813 """ 

814 text_raw = text 

815 

816 if escape is True: 

817 text_raw = MarkdownConverter.markdown_escape(text) 

818 

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

820 

821# Functions ******************************************************************** 

822 

823# Main *************************************************************************