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

213 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-26 12:41 +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 - 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/>. 

21 

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

23import hashlib 

24import os 

25import re 

26import shutil 

27import tempfile 

28from typing import Optional, Any 

29from trlc.ast import Implicit_Null, Record_Object, Record_Reference, String_Literal, Expression 

30from pyTRLCConverter.base_converter import BaseConverter 

31from pyTRLCConverter.markdown.document import MarkdownDocument 

32from pyTRLCConverter.markdown.element import Heading, Table, BulletList 

33from pyTRLCConverter.markdown.text import MarkdownText 

34from pyTRLCConverter.plantuml import PlantUML 

35from pyTRLCConverter.ret import Ret 

36from pyTRLCConverter.trlc_helper import TrlcAstWalker 

37from pyTRLCConverter.logger import log_verbose, log_error 

38 

39# Variables ******************************************************************** 

40 

41# Classes ********************************************************************** 

42 

43# pylint: disable-next=too-many-instance-attributes 

44class MarkdownConverter(BaseConverter): 

45 """ 

46 MarkdownConverter provides functionality for converting to a markdown format. 

47 

48 The converter builds a Markdown AST (MarkdownDocument made of block elements) 

49 while walking the TRLC symbols and writes the output file(s) only once the 

50 document is complete (in leave_file() for multiple-document mode and in 

51 finish() for single-document mode). 

52 """ 

53 

54 OUTPUT_FILE_NAME_DEFAULT = "output.md" 

55 TOP_LEVEL_DEFAULT = "Specification" 

56 

57 def __init__(self, args: Any) -> None: 

58 # lobster-trace: SwRequirements.sw_req_no_prj_spec 

59 # lobster-trace: SwRequirements.sw_req_markdown 

60 """ 

61 Initializes the converter. 

62 

63 Args: 

64 args (Any): The parsed program arguments. 

65 """ 

66 super().__init__(args) 

67 

68 # The path to the given output folder. 

69 self._out_path = args.out 

70 

71 # The excluded paths in normalized form. 

72 self._excluded_paths = [] 

73 

74 if args.exclude is not None: 

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

76 

77 # The Markdown document currently being built. In multiple-document mode a new 

78 # document is created per file, in single-document mode one document is shared. 

79 self._document: Optional[MarkdownDocument] = None 

80 

81 # The base level for the headings. Its the minimum level for the headings which depends 

82 # on the single/multiple document mode. 

83 self._base_level = 1 

84 

85 # A top level heading is always required to generate a compliant Markdown document. 

86 # In single document mode it will always be necessary. 

87 # In multiple document mode only if there is no top level section. 

88 self._is_top_level_heading_req = True 

89 

90 # The AST walker meta data for processing the record object fields. 

91 # This will hold the information about the current package, type and attribute being processed. 

92 self._ast_meta_data = None 

93 

94 self._plantuml_tmp_dir: Optional[tempfile.TemporaryDirectory] = None 

95 self._external_files: list = [] 

96 

97 @staticmethod 

98 def get_subcommand() -> str: 

99 # lobster-trace: SwRequirements.sw_req_markdown 

100 """ 

101 Return subcommand token for this converter. 

102 

103 Returns: 

104 str: Parser subcommand token 

105 """ 

106 return "markdown" 

107 

108 @staticmethod 

109 def get_description() -> str: 

110 # lobster-trace: SwRequirements.sw_req_markdown 

111 """ 

112 Return converter description. 

113 

114 Returns: 

115 str: Converter description 

116 """ 

117 return "Convert into markdown format." 

118 

119 @classmethod 

120 def register(cls, args_parser: Any) -> None: 

121 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

122 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

123 # lobster-trace: SwRequirements.sw_req_markdown_top_level_default 

124 # lobster-trace: SwRequirements.sw_req_markdown_top_level_custom 

125 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_default 

126 # lobster-trace: SwRequirements.sw_req_markdown_out_file_name_custom 

127 # lobster-trace: SwRequirements.sw_req_markdown_render_plantuml 

128 # lobster-trace: SwRequirements.sw_req_cli_render_plantuml 

129 """ 

130 Register converter specific argument parser. 

131 

132 Args: 

133 args_parser (Any): Argument parser 

134 """ 

135 super().register(args_parser) 

136 

137 assert BaseConverter._parser is not None 

138 

139 BaseConverter._parser.add_argument( 

140 "-e", 

141 "--empty", 

142 type=str, 

143 default=BaseConverter.EMPTY_ATTRIBUTE_DEFAULT, 

144 required=False, 

145 help="Every attribute value which is empty will output the string " \ 

146 f"(default = {BaseConverter.EMPTY_ATTRIBUTE_DEFAULT})." 

147 ) 

148 

149 BaseConverter._parser.add_argument( 

150 "-n", 

151 "--name", 

152 type=str, 

153 default=MarkdownConverter.OUTPUT_FILE_NAME_DEFAULT, 

154 required=False, 

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

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

157 "case a single document is generated." 

158 ) 

159 

160 BaseConverter._parser.add_argument( 

161 "-sd", 

162 "--single-document", 

163 action="store_true", 

164 required=False, 

165 default=False, 

166 help="Generate a single document instead of multiple files. The default is to generate multiple files." 

167 ) 

168 

169 BaseConverter._parser.add_argument( 

170 "-tl", 

171 "--top-level", 

172 type=str, 

173 default=MarkdownConverter.TOP_LEVEL_DEFAULT, 

174 required=False, 

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

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

177 ) 

178 

179 BaseConverter._parser.add_argument( 

180 "--render-plantuml", 

181 action="store_true", 

182 required=False, 

183 default=False, 

184 help="Render plantuml fenced code blocks as SVG image references. " 

185 "Without this option plantuml blocks are passed through unchanged." 

186 ) 

187 

188 def begin(self) -> Ret: 

189 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

190 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level 

191 """ 

192 Begin the conversion process. 

193 

194 Returns: 

195 Ret: Status 

196 """ 

197 assert self._document is None 

198 

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

200 result = BaseConverter.begin(self) 

201 

202 if result == Ret.OK: 

203 

204 # Single document mode? 

205 if self._args.single_document is True: 

206 log_verbose("Single document mode.") 

207 else: 

208 log_verbose("Multiple document mode.") 

209 

210 # Set the value for empty attributes. 

211 self._empty_attribute_value = self._args.empty 

212 

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

214 

215 if self._args.render_plantuml is True: 

216 # pylint: disable-next=consider-using-with 

217 self._plantuml_tmp_dir = tempfile.TemporaryDirectory( 

218 prefix="pyTRLCConverter_markdown_" 

219 ) 

220 

221 # Single document mode? 

222 if self._args.single_document is True: 

223 self._document = MarkdownDocument() 

224 

225 # The top level heading is always required in single document mode. 

226 self._document.add(Heading(self._args.top_level, 1)) 

227 self._is_top_level_heading_req = False 

228 

229 # All headings will be shifted by one level. 

230 self._base_level = self._base_level + 1 

231 

232 return result 

233 

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

235 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

236 """ 

237 Enter a file. 

238 

239 Args: 

240 file_name (str): File name 

241 

242 Returns: 

243 Ret: Status 

244 """ 

245 # Multiple document mode? 

246 if self._args.single_document is False: 

247 assert self._document is None 

248 

249 # A new document is built for each file. The very first written Markdown part 

250 # shall not have an empty line before, which the document handles implicitly. 

251 self._document = MarkdownDocument() 

252 self._is_top_level_heading_req = True 

253 

254 return Ret.OK 

255 

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

257 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

258 """ 

259 Leave a file. 

260 

261 Args: 

262 file_name (str): File name 

263 

264 Returns: 

265 Ret: Status 

266 """ 

267 result = Ret.OK 

268 

269 # Multiple document mode? 

270 if self._args.single_document is False: 

271 assert self._document is not None 

272 

273 file_name_md = self._file_name_trlc_to_md(file_name) 

274 result = self._write_document(file_name_md) 

275 

276 self._copy_external_files(self._out_path) 

277 self._external_files = [] 

278 self._document = None 

279 self._is_top_level_heading_req = True 

280 

281 return result 

282 

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

284 # lobster-trace: SwRequirements.sw_req_markdown_section 

285 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

286 """ 

287 Process the given section item. 

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

289 

290 Args: 

291 section (str): The section name 

292 level (int): The section indentation level 

293 

294 Returns: 

295 Ret: Status 

296 """ 

297 assert len(section) > 0 

298 assert self._document is not None 

299 

300 self._document.add(Heading(section, self._get_markdown_heading_level(level))) 

301 

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

303 self._is_top_level_heading_req = False 

304 

305 return Ret.OK 

306 

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

308 # lobster-trace: SwRequirements.sw_req_markdown_record 

309 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

310 """ 

311 Process the given record object in a generic way. 

312 

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

314 defined for the record type. 

315 

316 Args: 

317 record (Record_Object): The record object. 

318 level (int): The record level. 

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

320 If None, no translation is applied. 

321 

322 Returns: 

323 Ret: Status 

324 """ 

325 assert self._document is not None 

326 

327 self._add_top_level_heading_on_demand() 

328 

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

330 

331 def finish(self): 

332 # lobster-trace: SwRequirements.sw_req_markdown_single_doc_mode 

333 """ 

334 Finish the conversion process. 

335 

336 Returns: 

337 Ret: Status 

338 """ 

339 result = Ret.OK 

340 

341 # Single document mode? 

342 if self._args.single_document is True: 

343 assert self._document is not None 

344 

345 result = self._write_document(self._args.name) 

346 

347 self._copy_external_files(self._out_path) 

348 self._external_files = [] 

349 self._document = None 

350 

351 if self._plantuml_tmp_dir is not None: 

352 self._plantuml_tmp_dir.cleanup() 

353 self._plantuml_tmp_dir = None 

354 

355 return result 

356 

357 def _add_top_level_heading_on_demand(self) -> None: 

358 # lobster-trace: SwRequirements.sw_req_markdown_md_top_level 

359 # lobster-trace: SwRequirements.sw_req_markdown_sd_top_level 

360 """Add the top level heading to the document if necessary. 

361 """ 

362 assert self._document is not None 

363 

364 if self._is_top_level_heading_req is True: 

365 self._document.add(Heading(self._args.top_level, 1)) 

366 self._is_top_level_heading_req = False 

367 

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

369 # lobster-trace: SwRequirements.sw_req_markdown_record 

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

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

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

373 

374 Args: 

375 level (int): The TRLC object level. 

376 

377 Returns: 

378 int: Markdown heading level 

379 """ 

380 return self._base_level + level 

381 

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

383 # lobster-trace: SwRequirements.sw_req_markdown_multiple_doc_mode 

384 """ 

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

386 

387 Args: 

388 file_name_trlc (str): TRLC file name 

389 

390 Returns: 

391 str: Markdown file name 

392 """ 

393 file_name = os.path.basename(file_name_trlc) 

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

395 

396 return file_name 

397 

398 def _write_document(self, file_name: str) -> Ret: 

399 # lobster-trace: SwRequirements.sw_req_markdown_out_folder 

400 """ 

401 Write the current Markdown document to the output file. 

402 

403 Args: 

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

405 

406 Returns: 

407 Ret: Status 

408 """ 

409 assert self._document is not None 

410 

411 result = Ret.OK 

412 file_name_with_path = file_name 

413 

414 # Add path to the output file name. 

415 if 0 < len(self._out_path): 

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

417 

418 try: 

419 with open(file_name_with_path, "w", encoding="utf-8") as out_file: 

420 out_file.write(self._document.render()) 

421 except IOError as e: 

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

423 result = Ret.ERROR 

424 

425 return result 

426 

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

428 # lobster-trace: SwRequirements.sw_req_markdown_record 

429 """ 

430 Process the given implicit null value. 

431 

432 Returns: 

433 str: The implicit null value. 

434 """ 

435 return MarkdownText.escape(self._empty_attribute_value) 

436 

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

438 # lobster-trace: SwRequirements.sw_req_markdown_record 

439 """ 

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

441 

442 Args: 

443 record_reference (Record_Reference): The record reference value. 

444 

445 Returns: 

446 str: Markdown link to the record reference. 

447 """ 

448 return self._create_markdown_link_from_record_object_reference(record_reference) 

449 

450 def _on_string_literal(self, string_literal: String_Literal) -> str: 

451 # lobster-trace: SwRequirements.sw_req_markdown_string_format 

452 # lobster-trace: SwRequirements.sw_req_markdown_render_md 

453 # lobster-trace: SwRequirements.sw_req_markdown_render_gfm 

454 """ 

455 Process the given string literal value. 

456 

457 Args: 

458 string_literal (String_Literal): The string literal value. 

459 

460 Returns: 

461 str: The string literal value. 

462 """ 

463 result = string_literal.to_string() 

464 

465 if self._ast_meta_data is not None: 

466 package_name = self._ast_meta_data.get("package_name", "") 

467 type_name = self._ast_meta_data.get("type_name", "") 

468 attribute_name = self._ast_meta_data.get("attribute_name", "") 

469 

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

471 

472 return result 

473 

474 # pylint: disable-next=line-too-long 

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

476 # lobster-trace: SwRequirements.sw_req_markdown_record 

477 """ 

478 Create a Markdown link from a record reference. 

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

480 

481 Args: 

482 record_reference (Record_Reference): Record reference 

483 

484 Returns: 

485 str: Markdown link 

486 """ 

487 assert record_reference.target is not None 

488 

489 file_name = "" 

490 

491 # Single document mode? 

492 if self._args.single_document is True: 

493 file_name = self._args.name 

494 

495 # Is the link to a excluded file? 

496 for excluded_path in self._excluded_paths: 

497 

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

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

500 break 

501 

502 # Multiple document mode 

503 else: 

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

505 

506 record_name = record_reference.target.name 

507 

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

509 

510 return MarkdownText.link(str(record_reference.to_python_object()), anchor_tag) 

511 

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

513 # lobster-trace: SwRequirements.sw_req_markdown_record 

514 # lobster-trace: SwRequirements.sw_req_markdown_escape 

515 """ 

516 Dispatcher for all other expressions. 

517 

518 Args: 

519 expression (Expression): The expression to process. 

520 

521 Returns: 

522 str: The processed expression. 

523 """ 

524 return MarkdownText.escape(expression.to_string()) 

525 

526 def _get_trlc_ast_walker(self) -> TrlcAstWalker: 

527 # lobster-trace: SwRequirements.sw_req_markdown_record 

528 # lobster-trace: SwRequirements.sw_req_markdown_escape 

529 # lobster-trace: SwRequirements.sw_req_markdown_string_format 

530 """ 

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

532 a Markdown link. 

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

534 a Markdown list of links. 

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

536 

537 Returns: 

538 TrlcAstWalker: The TRLC AST walker. 

539 """ 

540 trlc_ast_walker = TrlcAstWalker() 

541 trlc_ast_walker.add_dispatcher( 

542 Implicit_Null, 

543 None, 

544 self._on_implict_null, 

545 None 

546 ) 

547 trlc_ast_walker.add_dispatcher( 

548 Record_Reference, 

549 None, 

550 self._on_record_reference, 

551 None 

552 ) 

553 trlc_ast_walker.add_dispatcher( 

554 String_Literal, 

555 None, 

556 self._on_string_literal, 

557 None 

558 ) 

559 trlc_ast_walker.set_other_dispatcher(self._other_dispatcher) 

560 

561 return trlc_ast_walker 

562 

563 def _render(self, package_name: str, type_name: str, attribute_name: str, attribute_value: str) -> str: 

564 # lobster-trace: SwRequirements.sw_req_markdown_string_format 

565 # lobster-trace: SwRequirements.sw_req_markdown_render_md 

566 # lobster-trace: SwRequirements.sw_req_markdown_render_gfm 

567 # lobster-trace: SwRequirements.sw_req_markdown_render_plantuml 

568 """Render the attribute value depending on its format. 

569 

570 Args: 

571 package_name (str): The package name. 

572 type_name (str): The type name. 

573 attribute_name (str): The attribute name. 

574 attribute_value (str): The attribute value. 

575 

576 Returns: 

577 str: The rendered attribute value. 

578 """ 

579 result = attribute_value 

580 

581 # If the attribute value is not already in Markdown format, it will be escaped. 

582 if self._render_cfg.is_format_md(package_name, type_name, attribute_name) is False and \ 

583 self._render_cfg.is_format_gfm(package_name, type_name, attribute_name) is False: 

584 

585 result = MarkdownText.escape(attribute_value) 

586 result = MarkdownText.lf2soft_return(result) 

587 

588 if self._args.render_plantuml is True: 

589 result = self._render_plantuml_blocks(result) 

590 

591 return result 

592 

593 def _render_plantuml_blocks(self, text: str) -> str: 

594 # lobster-trace: SwRequirements.sw_req_markdown_render_plantuml 

595 # lobster-trace: SwRequirements.sw_req_plantuml 

596 """Replace plantuml fenced code blocks with SVG image references. 

597 

598 Each ```plantuml ... ``` block is replaced by ``![](plantuml_<hash>.svg)``. 

599 The SVG is written to the temporary image directory and registered in 

600 ``_external_files`` for copying to the output folder. On failure an 

601 inline ``[PlantUML error: ...]`` text is emitted instead. 

602 

603 Args: 

604 text (str): Markdown text that may contain plantuml fenced blocks. 

605 

606 Returns: 

607 str: Text with plantuml blocks replaced. 

608 """ 

609 assert self._plantuml_tmp_dir is not None 

610 

611 def _replace(match: re.Match) -> str: 

612 diagram_source = match.group(1) 

613 try: 

614 plantuml = PlantUML() 

615 svg_bytes = plantuml.generate_to_bytes("svg", diagram_source) 

616 digest = hashlib.sha1(diagram_source.encode("utf-8")).hexdigest()[:12] 

617 local_name = f"plantuml_{digest}.svg" 

618 svg_path = os.path.join(self._plantuml_tmp_dir.name, local_name) 

619 with open(svg_path, "wb") as svg_file: 

620 svg_file.write(svg_bytes) 

621 self._external_files.append((svg_path, local_name)) 

622 return f"![]({local_name})" 

623 except (FileNotFoundError, OSError) as exc: 

624 return f"[PlantUML error: {exc}]" 

625 

626 return re.sub(r"```plantuml\n(.*?)```", _replace, text, flags=re.DOTALL) 

627 

628 def _copy_external_files(self, dest_dir: str) -> None: 

629 # lobster-trace: SwRequirements.sw_req_markdown_render_plantuml 

630 """Copy all collected external files to the given destination directory. 

631 

632 Args: 

633 dest_dir (str): Destination directory path. 

634 """ 

635 copied_sources = set() 

636 

637 for source_path, local_name in self._external_files: 

638 if source_path in copied_sources: 

639 continue 

640 

641 dest_path = os.path.join(dest_dir, local_name) 

642 

643 try: 

644 shutil.copy2(source_path, dest_path) 

645 copied_sources.add(source_path) 

646 except (OSError, IOError) as exc: 

647 log_error(f"Failed to copy external file '{source_path}': {exc}", False) 

648 

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

650 # lobster-trace: SwRequirements.sw_req_markdown_record 

651 """ 

652 Process the given record object. 

653 

654 Args: 

655 record (Record_Object): The record object. 

656 level (int): The record level. 

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

658 If None, no translation is applied. 

659 

660 Returns: 

661 Ret: Status 

662 """ 

663 assert self._document is not None 

664 

665 # The record name will be the heading. 

666 self._document.add(Heading(record.name, self._get_markdown_heading_level(level + 1))) 

667 

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

669 # First define the table column titles. 

670 table_column_titles = ["Attribute Name", "Attribute Value"] 

671 table_rows = [] 

672 

673 # Walk through the record object fields and build the table rows. 

674 trlc_ast_walker = self._get_trlc_ast_walker() 

675 

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

677 attribute_name = self._translate_attribute_name(translation, name) 

678 attribute_name = MarkdownText.escape(attribute_name) 

679 

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

681 # The result will be a string representation of the value. 

682 # If the value is an array of record references, the result will be a Markdown list of links. 

683 # If the value is a single record reference, the result will be a Markdown link. 

684 # If the value is a string literal, the result will be the string literal value that considers 

685 # its formatting. 

686 # Otherwise the result will be the attribute value in a proper format. 

687 self._ast_meta_data = { 

688 "package_name": record.n_package.name, 

689 "type_name": record.n_typ.name, 

690 "attribute_name": name 

691 } 

692 walker_result = trlc_ast_walker.walk(value) 

693 

694 attribute_value = "" 

695 if isinstance(walker_result, list): 

696 attribute_value = BulletList(walker_result, False).render() 

697 else: 

698 attribute_value = walker_result 

699 

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

701 table_rows.append([attribute_name, attribute_value]) 

702 

703 self._document.add(Table(table_column_titles, table_rows)) 

704 

705 return Ret.OK 

706 

707# Functions ******************************************************************** 

708 

709# Main *************************************************************************