Coverage for src / pyTRLCConverter / marko / rst_renderer.py: 75%

79 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-21 12:06 +0000

1"""reStructuredText renderer for Marko. 

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

23 

24from __future__ import annotations 

25from typing import TYPE_CHECKING, Any, cast 

26from marko import Renderer 

27 

28if TYPE_CHECKING: 

29 from . import block, inline 

30 

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

32 

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

34 

35# pylint: disable-next=too-many-public-methods 

36class RSTRenderer(Renderer): 

37 # lobster-trace: SwRequirements.sw_req_rst_render_md 

38 """Renderer for reStructuredText output.""" 

39 

40 def __init__(self) -> None: 

41 """ 

42 Initializes the RSTRenderer. 

43 """ 

44 super().__init__() 

45 self._list_indent_level = 0 

46 

47 def render_paragraph(self, element: block.Paragraph) -> str: 

48 """ 

49 Renders a paragraph element. 

50 

51 Args: 

52 element (block.Paragraph): The paragraph element to render. 

53 

54 Returns: 

55 str: The rendered paragraph as a string. 

56 """ 

57 return self.render_children(element) + "\n\n" 

58 

59 def render_list(self, element: block.List) -> str: 

60 """ 

61 Renders a list (ordered or unordered) element. 

62 

63 Args: 

64 element (block.List): The list element to render. 

65 

66 Returns: 

67 str: The rendered list as a string. 

68 """ 

69 items = [] 

70 

71 self._list_indent_level += 1 

72 

73 for index, child in enumerate(element.children): 

74 marker = f"{index + 1}." if element.ordered else "-" 

75 item = self.render_list_item(child, marker) 

76 items.append(item) 

77 

78 self._list_indent_level -= 1 

79 

80 return "\n".join(items) + "\n" 

81 

82 def render_list_item(self, element: block.ListItem, marker="-") -> str: 

83 """ 

84 Renders a list item element. 

85 

86 Args: 

87 element (block.ListItem): The list item element to render. 

88 marker (str, optional): The marker to use for the list item. Defaults to "*". 

89 

90 Returns: 

91 str: The rendered list item as a string. 

92 """ 

93 indent = 2 

94 content = self.render_children(element) 

95 

96 return f"{' ' * indent * (self._list_indent_level - 1)}{marker} {content}" 

97 

98 def render_quote(self, element: block.Quote) -> str: 

99 """ 

100 Renders a blockquote element. 

101 

102 Args: 

103 element (block.Quote): The blockquote element to render. 

104 

105 Returns: 

106 str: The rendered blockquote as a string. 

107 """ 

108 quote = self.render_children(element) 

109 quoted = "\n".join([f" {line}" if line.strip() else "" for line in quote.splitlines()]) 

110 

111 return quoted + "\n\n" 

112 

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

114 """ 

115 Renders a fenced code block element. 

116 

117 Args: 

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

119 

120 Returns: 

121 str: The rendered fenced code block as a string. 

122 """ 

123 lang = element.lang or "" 

124 code = element.children[0].children # type: ignore 

125 

126 return f".. code-block:: {lang}\n\n " + "\n ".join(code.splitlines()) + "\n\n" 

127 

128 def render_code_block(self, element: block.CodeBlock) -> str: 

129 """ 

130 Renders a code block element. 

131 

132 Args: 

133 element (block.CodeBlock): The code block element to render. 

134 

135 Returns: 

136 str: The rendered code block as a string. 

137 """ 

138 return self.render_fenced_code(cast("block.FencedCode", element)) 

139 

140 def render_html_block(self, element: block.HTMLBlock) -> str: 

141 """ 

142 Renders a raw HTML block element. 

143 

144 Args: 

145 element (block.HTMLBlock): The HTML block element to render. 

146 

147 Returns: 

148 str: The rendered HTML block as a string. 

149 """ 

150 # reStructuredText does not support raw HTML, so output as a literal block. 

151 body = element.body 

152 

153 return "::\n\n " + "\n ".join(body.splitlines()) + "\n\n" 

154 

155 # pylint: disable-next=unused-argument 

156 def render_thematic_break(self, element: block.ThematicBreak) -> str: 

157 """ 

158 Renders a thematic break (horizontal rule) element. 

159 

160 Args: 

161 element (block.ThematicBreak): The thematic break element to render. 

162 

163 Returns: 

164 str: The rendered thematic break as a string. 

165 """ 

166 return "\n----\n\n" 

167 

168 def render_heading(self, element: block.Heading) -> str: 

169 """ 

170 Renders a heading element. 

171 

172 Args: 

173 element (block.Heading): The heading element to render. 

174 

175 Returns: 

176 str: The rendered heading as a string. 

177 """ 

178 text = self.render_children(element) 

179 underline = { 

180 1: "=", 

181 2: "-", 

182 3: "~", 

183 4: "^", 

184 5: '"', 

185 6: "'" 

186 }.get(element.level, "-") 

187 

188 return f"{text}\n{underline * len(text)}\n\n" 

189 

190 def render_setext_heading(self, element: block.SetextHeading) -> str: 

191 """ 

192 Renders a setext heading element. 

193 

194 Args: 

195 element (block.SetextHeading): The setext heading element to render. 

196 

197 Returns: 

198 str: The rendered setext heading as a string. 

199 """ 

200 return self.render_heading(cast("block.Heading", element)) 

201 

202 # pylint: disable-next=unused-argument 

203 def render_blank_line(self, element: block.BlankLine) -> str: 

204 """ 

205 Renders a blank line element. 

206 

207 Args: 

208 element (block.BlankLine): The blank line element to render. 

209 

210 Returns: 

211 str: The rendered blank line as a string. 

212 """ 

213 return "\n" 

214 

215 # pylint: disable-next=unused-argument 

216 def render_link_ref_def(self, element: block.LinkRefDef) -> str: 

217 """ 

218 Renders a link reference definition element. 

219 

220 Args: 

221 element (block.LinkRefDef): The link reference definition element to render. 

222 

223 Returns: 

224 str: The rendered link reference definition as a string. 

225 """ 

226 # reStructuredText uses reference links differently than Markdown. 

227 # It shall not be rendered in the document. 

228 return "" 

229 

230 def render_emphasis(self, element: inline.Emphasis) -> str: 

231 """ 

232 Renders an emphasis (italic) element. 

233 

234 Args: 

235 element (inline.Emphasis): The emphasis element to render. 

236 

237 Returns: 

238 str: The rendered emphasis as a string. 

239 """ 

240 return f"*{self.render_children(element)}*" 

241 

242 def render_strong_emphasis(self, element: inline.StrongEmphasis) -> str: 

243 """ 

244 Renders a strong emphasis (bold) element. 

245 

246 Args: 

247 element (inline.StrongEmphasis): The strong emphasis element to render. 

248 

249 Returns: 

250 str: The rendered strong emphasis as a string. 

251 """ 

252 return f"**{self.render_children(element)}**" 

253 

254 def render_inline_html(self, element: inline.InlineHTML) -> str: 

255 """ 

256 Renders an inline HTML element. 

257 

258 Args: 

259 element (inline.InlineHTML): The inline HTML element to render. 

260 

261 Returns: 

262 str: The rendered inline HTML as a string. 

263 """ 

264 # Output as literal block. 

265 html_content = cast(str, element.children) 

266 return f"``{html_content}``" 

267 

268 def render_plain_text(self, element: Any) -> str: 

269 """ 

270 Renders plain text or any element with string children. 

271 

272 Args: 

273 element (Any): The element to render. 

274 

275 Returns: 

276 str: The rendered plain text as a string. 

277 """ 

278 if isinstance(element.children, str): 

279 return element.children 

280 

281 return self.render_children(element) 

282 

283 def render_link(self, element: inline.Link) -> str: 

284 """ 

285 Renders a link element. 

286 

287 Args: 

288 element (inline.Link): The link element to render. 

289 

290 Returns: 

291 str: The rendered link as a string. 

292 """ 

293 body = self.render_children(element) 

294 url = element.dest 

295 title = f" ({element.title})" if element.title else "" 

296 

297 return f"`{body} <{url}>`_{title}" 

298 

299 def render_auto_link(self, element: inline.AutoLink) -> str: 

300 """ 

301 Renders an auto link element. 

302 

303 Args: 

304 element (inline.AutoLink): The auto link element to render. 

305 

306 Returns: 

307 str: The rendered auto link as a string. 

308 """ 

309 return self.render_link(cast("inline.Link", element)) 

310 

311 def render_image(self, element: inline.Image) -> str: 

312 """ 

313 Renders an image element. 

314 

315 Args: 

316 element (inline.Image): The image element to render. 

317 

318 Returns: 

319 str: The rendered image as a string. 

320 """ 

321 url = element.dest 

322 alt = self.render_children(element) 

323 title = f" :alt: {alt}" if alt else "" 

324 extra_title = f" :title: {element.title}" if element.title else "" 

325 

326 return f".. image:: {url}\n{title}\n{extra_title}\n" 

327 

328 def render_literal(self, element: inline.Literal) -> str: 

329 """ 

330 Renders a literal (inline code) element. 

331 

332 Args: 

333 element (inline.Literal): The literal element to render. 

334 

335 Returns: 

336 str: The rendered literal as a string. 

337 """ 

338 return self.render_raw_text(cast("inline.RawText", element)) 

339 

340 def render_raw_text(self, element: inline.RawText) -> str: 

341 """ 

342 Renders a raw text element. 

343 

344 Args: 

345 element (inline.RawText): The raw text element to render. 

346 

347 Returns: 

348 str: The rendered raw text as a string. 

349 """ 

350 return f"{element.children}" 

351 

352 # pylint: disable-next=unused-argument 

353 def render_line_break(self, element: inline.LineBreak) -> str: 

354 """ 

355 Renders a line break element. 

356 

357 Args: 

358 element (inline.LineBreak): The line break element to render. 

359 

360 Returns: 

361 str: The rendered line break as a string. 

362 """ 

363 return "\n" 

364 

365 def render_code_span(self, element: inline.CodeSpan) -> str: 

366 """ 

367 Renders a code span (inline code) element. 

368 

369 Args: 

370 element (inline.CodeSpan): The code span element to render. 

371 

372 Returns: 

373 str: The rendered code span as a string. 

374 """ 

375 return f"``{cast(str, element.children)}``" 

376 

377# Functions ******************************************************************** 

378 

379# Main *************************************************************************