Coverage for src / pyTRLCConverter / marko / md2rst_renderer.py: 74%

81 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 12:20 +0000

1"""reStructuredText renderer for Marko. 

2 It is used to convert CommonMark AST to reStructuredText format. 

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 

26from typing import TYPE_CHECKING, Any, cast 

27from marko import Renderer 

28 

29if TYPE_CHECKING: 

30 from . import block, inline 

31 

32# Variables ******************************************************************** 

33 

34# Classes ********************************************************************** 

35 

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

37class Md2RstRenderer(Renderer): 

38 # lobster-trace: SwRequirements.sw_req_rst_render_md 

39 """ 

40 Renderer for reStructuredText output. 

41 It is used to convert CommonMark Markdown to reStructuredText format. 

42 """ 

43 

44 def __init__(self) -> None: 

45 """ 

46 Initializes the renderer. 

47 """ 

48 super().__init__() 

49 self._list_indent_level = 0 

50 

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

52 """ 

53 Renders a paragraph element. 

54 

55 Args: 

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

57 

58 Returns: 

59 str: The rendered paragraph as a string. 

60 """ 

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

62 

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

64 """ 

65 Renders a list (ordered or unordered) element. 

66 

67 Args: 

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

69 

70 Returns: 

71 str: The rendered list as a string. 

72 """ 

73 items = [] 

74 

75 self._list_indent_level += 1 

76 

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

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

79 item = self.render_list_item(child, marker) 

80 items.append(item) 

81 

82 self._list_indent_level -= 1 

83 

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

85 

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

87 """ 

88 Renders a list item element. 

89 

90 Args: 

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

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

93 

94 Returns: 

95 str: The rendered list item as a string. 

96 """ 

97 indent = 2 

98 content = self.render_children(element) 

99 

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

101 

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

103 """ 

104 Renders a blockquote element. 

105 

106 Args: 

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

108 

109 Returns: 

110 str: The rendered blockquote as a string. 

111 """ 

112 quote = self.render_children(element) 

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

114 

115 return quoted + "\n\n" 

116 

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

118 """ 

119 Renders a fenced code block element. 

120 

121 Args: 

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

123 

124 Returns: 

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

126 """ 

127 lang = element.lang or "" 

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

129 

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

131 

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

133 """ 

134 Renders a code block element. 

135 

136 Args: 

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

138 

139 Returns: 

140 str: The rendered code block as a string. 

141 """ 

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

143 

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

145 """ 

146 Renders a raw HTML block element. 

147 

148 Args: 

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

150 

151 Returns: 

152 str: The rendered HTML block as a string. 

153 """ 

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

155 body = element.body 

156 

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

158 

159 # pylint: disable-next=unused-argument 

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

161 """ 

162 Renders a thematic break (horizontal rule) element. 

163 

164 Args: 

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

166 

167 Returns: 

168 str: The rendered thematic break as a string. 

169 """ 

170 return "\n----\n\n" 

171 

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

173 """ 

174 Renders a heading element. 

175 

176 Args: 

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

178 

179 Returns: 

180 str: The rendered heading as a string. 

181 """ 

182 text = self.render_children(element) 

183 underline = { 

184 1: "=", 

185 2: "-", 

186 3: "~", 

187 4: "^", 

188 5: '"', 

189 6: "'" 

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

191 

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

193 

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

195 """ 

196 Renders a setext heading element. 

197 

198 Args: 

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

200 

201 Returns: 

202 str: The rendered setext heading as a string. 

203 """ 

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

205 

206 # pylint: disable-next=unused-argument 

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

208 """ 

209 Renders a blank line element. 

210 

211 Args: 

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

213 

214 Returns: 

215 str: The rendered blank line as a string. 

216 """ 

217 return "\n" 

218 

219 # pylint: disable-next=unused-argument 

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

221 """ 

222 Renders a link reference definition element. 

223 

224 Args: 

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

226 

227 Returns: 

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

229 """ 

230 # reStructuredText uses reference links differently than Markdown. 

231 # It shall not be rendered in the document. 

232 return "" 

233 

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

235 """ 

236 Renders an emphasis (italic) element. 

237 

238 Args: 

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

240 

241 Returns: 

242 str: The rendered emphasis as a string. 

243 """ 

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

245 

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

247 """ 

248 Renders a strong emphasis (bold) element. 

249 

250 Args: 

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

252 

253 Returns: 

254 str: The rendered strong emphasis as a string. 

255 """ 

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

257 

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

259 """ 

260 Renders an inline HTML element. 

261 

262 Args: 

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

264 

265 Returns: 

266 str: The rendered inline HTML as a string. 

267 """ 

268 # Output as literal block. 

269 html_content = cast(str, element.children) 

270 return f"``{html_content}``" 

271 

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

273 """ 

274 Renders plain text or any element with string children. 

275 

276 Args: 

277 element (Any): The element to render. 

278 

279 Returns: 

280 str: The rendered plain text as a string. 

281 """ 

282 if isinstance(element.children, str): 

283 return element.children 

284 

285 return self.render_children(element) 

286 

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

288 """ 

289 Renders a link element. 

290 

291 Args: 

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

293 

294 Returns: 

295 str: The rendered link as a string. 

296 """ 

297 body = self.render_children(element) 

298 url = element.dest 

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

300 

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

302 

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

304 """ 

305 Renders an auto link element. 

306 

307 Args: 

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

309 

310 Returns: 

311 str: The rendered auto link as a string. 

312 """ 

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

314 

315 def render_url(self, element: Any) -> str: 

316 """Renders a GitHub Flavored Markdown URL element. 

317 

318 Args: 

319 element (Any): The URL element. 

320 

321 Returns: 

322 str: The rendered URL as an RST link. 

323 """ 

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

325 

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

327 """ 

328 Renders an image element. 

329 

330 Args: 

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

332 

333 Returns: 

334 str: The rendered image as a string. 

335 """ 

336 url = element.dest 

337 alt = self.render_children(element) 

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

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

340 

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

342 

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

344 """ 

345 Renders a literal (inline code) element. 

346 

347 Args: 

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

349 

350 Returns: 

351 str: The rendered literal as a string. 

352 """ 

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

354 

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

356 """ 

357 Renders a raw text element. 

358 

359 Args: 

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

361 

362 Returns: 

363 str: The rendered raw text as a string. 

364 """ 

365 return f"{element.children}" 

366 

367 # pylint: disable-next=unused-argument 

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

369 """ 

370 Renders a line break element. 

371 

372 Args: 

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

374 

375 Returns: 

376 str: The rendered line break as a string. 

377 """ 

378 return "\n" 

379 

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

381 """ 

382 Renders a code span (inline code) element. 

383 

384 Args: 

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

386 

387 Returns: 

388 str: The rendered code span as a string. 

389 """ 

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

391 

392# Functions ******************************************************************** 

393 

394# Main *************************************************************************