Coverage for src/pyTRLCConverter/marko/md2reqif_renderer.py: 100%

30 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-26 12:41 +0000

1"""ReqIF Renderer for Marko. 

2 It is used to convert CommonMark AST to ReqIF-compatible XHTML. 

3 

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

5""" 

6 

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

8# Copyright (c) 2024 - 2026 NewTec GmbH 

9# 

10# This file is part of pyTRLCConverter program. 

11# 

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

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

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

15# 

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

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

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

19# 

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

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

22 

23# Imports ********************************************************************** 

24 

25from __future__ import annotations 

26import hashlib 

27import html 

28import os 

29from typing import TYPE_CHECKING, Optional 

30from marko.html_renderer import HTMLRenderer 

31from pyTRLCConverter.plantuml import PlantUML 

32 

33if TYPE_CHECKING: 

34 from marko import block 

35 

36# Variables ******************************************************************** 

37 

38# Classes ********************************************************************** 

39 

40 

41class Md2ReqifRenderer(HTMLRenderer): 

42 # lobster-trace: SwRequirements.sw_req_reqif_render_md 

43 # lobster-trace: SwRequirements.sw_req_reqif_render_plantuml 

44 # lobster-trace: SwRequirements.sw_req_plantuml 

45 """Renderer for ReqIF XHTML output. 

46 

47 Extends marko's CommonMark HTML renderer so that fenced code blocks tagged 

48 ``plantuml`` are rendered as embedded SVG images referenced via XHTML 

49 ``<object>`` elements. Other fenced code blocks are rendered by the parent 

50 class as ``<pre><code>`` blocks. 

51 """ 

52 

53 # Destination directory where generated SVG images are written. The 

54 # converter is responsible for setting this before each ``convert()`` call 

55 # and for ensuring the directory exists and survives until the external 

56 # files have been copied to the final output location. 

57 image_dir: Optional[str] = None 

58 

59 # List of ``(source_path, local_name)`` tuples collected during rendering. 

60 # The converter merges these into its own ``_external_files`` list so the 

61 # images are copied alongside the ReqIF document. 

62 external_files: Optional[list] = None 

63 

64 def render_fenced_code(self, element: "block.FencedCode") -> str: 

65 # lobster-trace: SwRequirements.sw_req_reqif_render_md 

66 # lobster-trace: SwRequirements.sw_req_plantuml 

67 """Render a fenced code block as ReqIF XHTML. 

68 

69 If the language tag is ``plantuml``, the diagram source is rendered as 

70 an embedded SVG image referenced via an XHTML ``<object>`` element. 

71 If PlantUML is not available, an error string is emitted instead. 

72 For all other language tags the block is rendered by the parent class. 

73 

74 Args: 

75 element (block.FencedCode): The fenced code block element to render. 

76 

77 Returns: 

78 str: XHTML representation of the fenced code block. 

79 """ 

80 if element.lang == "plantuml": 

81 return self._render_plantuml(element.children[0].children) 

82 

83 return super().render_fenced_code(element) 

84 

85 def _render_plantuml(self, diagram_source: str) -> str: 

86 # lobster-trace: SwRequirements.sw_req_reqif_render_md 

87 # lobster-trace: SwRequirements.sw_req_plantuml 

88 """Render a PlantUML diagram as an embedded SVG reference. 

89 

90 Generates SVG bytes via the PlantUML tool, writes them to a temporary 

91 file under :attr:`image_dir`, registers the file in 

92 :attr:`external_files` so the converter can copy it next to the ReqIF 

93 document, and returns an XHTML ``<object>`` element referencing the 

94 image by its local name. 

95 

96 On failure (PlantUML not available, server error, ...) an error 

97 paragraph is emitted instead. 

98 

99 Args: 

100 diagram_source (str): The PlantUML diagram source text. 

101 

102 Returns: 

103 str: XHTML fragment referencing the generated image, or an error 

104 paragraph if image generation failed. 

105 """ 

106 assert Md2ReqifRenderer.image_dir is not None 

107 assert Md2ReqifRenderer.external_files is not None 

108 

109 try: 

110 plantuml = PlantUML() 

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

112 

113 # Derive a stable, content-addressed file name from the diagram 

114 # source so identical diagrams share a single SVG and different 

115 # diagrams never collide when several documents are written to the 

116 # same output directory (multi-document mode). 

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

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

119 svg_path = os.path.join(Md2ReqifRenderer.image_dir, local_name) 

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

121 svg_file.write(svg_bytes) 

122 

123 Md2ReqifRenderer.external_files.append((svg_path, local_name)) 

124 

125 result = f'<p><object type="image/svg+xml" data="{html.escape(local_name)}"></object></p>\n' 

126 except (FileNotFoundError, OSError) as exc: 

127 result = f"<p>[PlantUML error: {html.escape(str(exc))}]</p>\n" 

128 

129 return result 

130 

131# Functions ******************************************************************** 

132 

133# Main *************************************************************************