MDit 0.0.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: MDit
3
+ Version: 0.0.0.dev0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: IPython
6
+ Requires-Dist: PyProtocol
7
+ Requires-Dist: markdown-it-py
8
+ Requires-Dist: mdit-py-plugins
9
+ Requires-Dist: linkify-it-py
10
+ Requires-Dist: readme-renderer
11
+ Requires-Dist: cmarkgfm
@@ -0,0 +1 @@
1
+ # MDit
@@ -0,0 +1,31 @@
1
+
2
+ [build-system]
3
+ requires = ["setuptools>=61.0", "versioningit"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+
7
+ # ----------------------------------------- setuptools -------------------------------------------
8
+ [tool.setuptools]
9
+ include-package-data = true
10
+ zip-safe = false
11
+
12
+ [tool.setuptools.packages.find]
13
+ where = ["src"]
14
+ namespaces = true
15
+
16
+
17
+ # ----------------------------------------- Project Metadata -------------------------------------
18
+ #
19
+ [project]
20
+ version = "0.0.0.dev0"
21
+ name = "MDit"
22
+ requires-python = ">=3.10"
23
+ dependencies = [
24
+ "IPython",
25
+ "PyProtocol",
26
+ "markdown-it-py",
27
+ "mdit-py-plugins",
28
+ "linkify-it-py",
29
+ "readme-renderer",
30
+ "cmarkgfm",
31
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ Metadata-Version: 2.1
2
+ Name: MDit
3
+ Version: 0.0.0.dev0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: IPython
6
+ Requires-Dist: PyProtocol
7
+ Requires-Dist: markdown-it-py
8
+ Requires-Dist: mdit-py-plugins
9
+ Requires-Dist: linkify-it-py
10
+ Requires-Dist: readme-renderer
11
+ Requires-Dist: cmarkgfm
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/MDit.egg-info/PKG-INFO
4
+ src/MDit.egg-info/SOURCES.txt
5
+ src/MDit.egg-info/dependency_links.txt
6
+ src/MDit.egg-info/not-zip-safe
7
+ src/MDit.egg-info/requires.txt
8
+ src/MDit.egg-info/top_level.txt
9
+ src/mdit/__init__.py
10
+ src/mdit/container.py
11
+ src/mdit/display.py
12
+ src/mdit/element.py
13
+ src/mdit/parse.py
14
+ src/mdit/render.py
@@ -0,0 +1,7 @@
1
+ IPython
2
+ PyProtocol
3
+ markdown-it-py
4
+ mdit-py-plugins
5
+ linkify-it-py
6
+ readme-renderer
7
+ cmarkgfm
@@ -0,0 +1 @@
1
+ mdit
@@ -0,0 +1,6 @@
1
+ """Generate and process Markdown content.
2
+
3
+ References
4
+ ----------
5
+ - [GitHub Flavored Markdown Spec](https://github.github.com/gfm/)
6
+ """
@@ -0,0 +1,71 @@
1
+ from typing import Type as _Type
2
+
3
+ from pyprotocol import Stringable as _Stringable
4
+
5
+
6
+ ContentType = _Stringable
7
+ ContentInputType = (
8
+ dict[str | int, ContentType]
9
+ | list[ContentType]
10
+ | tuple[ContentType]
11
+ | None
12
+ )
13
+
14
+
15
+ class Container(_Stringable):
16
+
17
+ def __init__(self, *unlabeled_contents: ContentType, **labeled_contents: ContentType):
18
+ self._data = {}
19
+ self.add(*unlabeled_contents, **labeled_contents)
20
+ return
21
+
22
+ def add(self, *unlabeled_contents: ContentType, **labeled_contents: ContentType) -> list[int] | None:
23
+ if labeled_contents:
24
+ for key, value in labeled_contents.items():
25
+ if key in self._data:
26
+ raise ValueError("Key already exists in content.")
27
+ self._data[key] = value
28
+ if unlabeled_contents:
29
+ first_available_int_key = max(key for key in self._data.keys() if isinstance(key, int)) + 1
30
+ for idx, elem in enumerate(unlabeled_contents):
31
+ self._data[first_available_int_key + idx] = elem
32
+ return list(range(first_available_int_key, first_available_int_key + len(unlabeled_contents)))
33
+ return
34
+
35
+ def get(self, key: str | int, default=None):
36
+ return self._data.get(key, default)
37
+
38
+ def keys(self):
39
+ return self._data.keys()
40
+
41
+ def values(self):
42
+ return self._data.values()
43
+
44
+ def items(self):
45
+ return self._data.items()
46
+
47
+ def __getitem__(self, item):
48
+ return self._data[item]
49
+
50
+ def __setitem__(self, key, value):
51
+ self._data[key] = value
52
+ return
53
+
54
+ def __contains__(self, item):
55
+ return item in self._data
56
+
57
+ def __bool__(self):
58
+ return bool(self._data)
59
+
60
+
61
+ def create(
62
+ content: ContentInputType,
63
+ container_class: _Type[Container] = Container
64
+ ) -> Container:
65
+ if not content:
66
+ return container_class()
67
+ if isinstance(content, dict):
68
+ return container_class(**content)
69
+ if isinstance(content, (list, tuple)):
70
+ return container_class(*content)
71
+ return container_class(content)
@@ -0,0 +1,52 @@
1
+ """Display HTML and Markdown content in web browser or IPython notebook."""
2
+
3
+ import webbrowser as _webbrowser
4
+ import tempfile as _tempfile
5
+ import time as _time
6
+ from pathlib import Path as _Path
7
+
8
+ from IPython import display as _display
9
+
10
+
11
+ def browser(content: str) -> None:
12
+ """Display HTML content in a web browser.
13
+
14
+ This function writes the content to a temporary file and opens it in the system's default web browser.
15
+ It then waits for 10 seconds (ensuring the browser has enough time to load the content)
16
+ before deleting the temporary file.
17
+
18
+ Parameters
19
+ ----------
20
+ content : str
21
+ HTML content to display.
22
+ """
23
+ with _tempfile.NamedTemporaryFile('w', delete=False, suffix='.html') as temp_file:
24
+ temp_file.write(content)
25
+ temp_file.flush()
26
+ temp_filepath = temp_file.name
27
+ _webbrowser.open(f'file://{temp_filepath}')
28
+ _time.sleep(10)
29
+ _Path(temp_filepath).unlink()
30
+ return
31
+
32
+
33
+ def ipython(content: str, as_md: bool = False) -> None:
34
+ """Display HTML or Markdown content in an IPython notebook.
35
+
36
+ This function uses the `IPython.display` module to render the content
37
+ in the current cell of an IPython notebook.
38
+
39
+ Parameters
40
+ ----------
41
+ content : str
42
+ HTML or Markdown content to display.
43
+ as_md : bool, default: False
44
+ If True, the function uses the `IPython.display.Markdown` renderer,
45
+ otherwise (by default) it uses the `IPython.display.HTML` renderer
46
+ """
47
+ if ipython:
48
+ renderer = _display.Markdown if as_md else _display.HTML
49
+ _display.display(renderer(content))
50
+ return
51
+
52
+ return
@@ -0,0 +1,558 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING as _TYPE_CHECKING
3
+
4
+ if _TYPE_CHECKING:
5
+ from typing import Literal
6
+
7
+
8
+ class Element:
9
+ def __init__(
10
+ self,
11
+ block: bool,
12
+ leaf: bool = True,
13
+ contents: _ElementContentInputType = None,
14
+ newlines_before: int | None = None,
15
+ newlines_after: int | None = None,
16
+ ):
17
+ super().__init__(contents=contents)
18
+ self._block = block
19
+ self._leaf = leaf
20
+ self.newlines_before = newlines_before
21
+ self.newlines_after = newlines_after
22
+ return
23
+
24
+ def __str__(self):
25
+ if not self._block:
26
+ if any(not isinstance(elem, str) for elem in self._content.values()):
27
+ raise ValueError("Inline elements must have string content.")
28
+ elif self._leaf:
29
+ if any(isinstance(elem, Element) and elem.is_block for elem in self._content.values()):
30
+ raise ValueError("Leaf block elements cannot contain block content.")
31
+ content = "".join(str(elem) for elem in self._content.values())
32
+ md = self._md.replace("${{content}}", content)
33
+ newlines_before, newlines_after = [
34
+ newlines_count if isinstance(newlines_count, int) else (1 if self._block else 0)
35
+ for newlines_count in (self.newlines_before, self.newlines_after)
36
+ ]
37
+ return f"{newlines_before * '\n'}{md}{newlines_after * '\n'}"
38
+
39
+ @property
40
+ def _md(self) -> str:
41
+ return "${{content}}"
42
+
43
+ @property
44
+ def is_block(self) -> bool:
45
+ return self._block
46
+
47
+ @property
48
+ def is_leaf(self) -> bool:
49
+ return self._leaf
50
+
51
+ def display(self, ipython: bool = True, as_md: bool = True) -> None:
52
+ """Display the element in an IPython notebook."""
53
+ super().display(ipython=ipython, as_md=as_md)
54
+ return
55
+
56
+
57
+ class ThematicBreak(Element):
58
+ def __init__(self, char: Literal["-", "_", "*"] = "-"):
59
+ super().__init__(block=True)
60
+ self.char = char
61
+ return
62
+
63
+ @property
64
+ def char(self):
65
+ return self._char
66
+
67
+ @char.setter
68
+ def char(self, value: _Literal["-", "_", "*"]):
69
+ if value not in ("-", "_", "*"):
70
+ raise ValueError("Invalid thematic break character.")
71
+ self._char = value
72
+ return
73
+
74
+ @property
75
+ def _md(self) -> str:
76
+ return self.char * 3
77
+
78
+
79
+ class ATXHeading(Element):
80
+ def __init__(self, level: _Literal[1, 2, 3, 4, 5, 6], contents: _ElementContentInputType = ""):
81
+ super().__init__(block=True, leaf=True, contents=contents)
82
+ self._level = level
83
+ return
84
+
85
+ @property
86
+ def level(self):
87
+ return self._level
88
+
89
+ @level.setter
90
+ def level(self, value: _Literal[1, 2, 3, 4, 5, 6]):
91
+ if value not in (1, 2, 3, 4, 5, 6):
92
+ raise ValueError("Invalid heading level.")
93
+ self._level = value
94
+ return
95
+
96
+ @property
97
+ def _md(self) -> str:
98
+ return f"{'#' * self.level} ${{content}}"
99
+
100
+
101
+ class FieldListElement(Element):
102
+ def __init__(self, name: _ElementContentType, body: _ElementContentInputType = "", indent_size: int = 4):
103
+ super().__init__(block=True, leaf=False, contents=body)
104
+ self.name = name
105
+ self.indent_size = indent_size
106
+ return
107
+
108
+ @property
109
+ def _md(self) -> str:
110
+ body = "".join(str(elem) for elem in self._content.values())
111
+ first_line, *lines = body.strip().split("\n")
112
+ body = "\n".join([first_line] + [f"{' ' * self.indent_size}{line}" for line in lines])
113
+ return f":{self.name}: {body}".strip()
114
+
115
+
116
+ class FieldList(Element):
117
+ def __init__(self, elements: list[FieldListElement], indent_size: int = 4):
118
+ super().__init__(block=True, leaf=False)
119
+ self.indent_size = indent_size
120
+ self.elements = elements
121
+ return
122
+
123
+ @property
124
+ def _md(self) -> str:
125
+ elements_md = []
126
+ for elem in self.elements:
127
+ indent_orig = elem.indent_size
128
+ elem.indent_size = self.indent_size
129
+ elements_md.append(elem._md)
130
+ elem.indent_size = indent_orig
131
+ return "\n".join(elements_md)
132
+
133
+
134
+ class HTMLBlock(Element):
135
+
136
+ def __init__(self, contents: _ElementContentInputType = None):
137
+ super().__init__(block=True, leaf=True, contents=contents, newlines_after=2)
138
+ return
139
+
140
+ @property
141
+ def _md(self) -> str:
142
+ return "${{content}}"
143
+
144
+
145
+ class CodeFence(Element):
146
+
147
+ def __init__(
148
+ self,
149
+ contents: _ElementContentInputType = None,
150
+ info: _Stringable = "",
151
+ fence: _Literal["`", "~", ":"] = "`",
152
+ ):
153
+ super().__init__(block=True, leaf=False, contents=contents)
154
+ self._info = info
155
+ self.fence = fence
156
+ return
157
+
158
+ @property
159
+ def info(self) -> _Stringable:
160
+ return self._info
161
+
162
+ @info.setter
163
+ def info(self, value: _Stringable):
164
+ if "\n" in str(value):
165
+ raise ValueError("Info string cannot contain newlines.")
166
+ self._info = value
167
+ return
168
+
169
+ @property
170
+ def fence(self):
171
+ return self._fence
172
+
173
+ @fence.setter
174
+ def fence(self, value: _Literal["`", "~", ":"]):
175
+ if value not in ("`", "~", ":"):
176
+ raise ValueError("Invalid code fence character.")
177
+ self._fence = value
178
+ return
179
+
180
+ @property
181
+ def _md(self) -> str:
182
+ return f"{self._start_line}\n${{content}}\n{self._end_line}"
183
+
184
+ @property
185
+ def fence_count(self):
186
+ return max(
187
+ [child.fence_count for child in self.content.values() if isinstance(child, CodeFence)],
188
+ default=3
189
+ )
190
+
191
+ @property
192
+ def _start_line(self) -> str:
193
+ return f"{self.fence * self.fence_count}{self.info}"
194
+
195
+ @property
196
+ def _end_line(self) -> str:
197
+ return self.fence * self.fence_count
198
+
199
+
200
+ class Directive(CodeFence):
201
+ def __init__(
202
+ self,
203
+ name: _Stringable,
204
+ contents: _ElementContentInputType = None,
205
+ arg: _Stringable = "",
206
+ options: dict[_Stringable, _Stringable] | None = None,
207
+ fence: _Literal["`", "~", ":"] = "`",
208
+ ):
209
+ super().__init__(contents=contents, fence=fence)
210
+ self.name = name
211
+ self.arg = arg
212
+ self.options = options or {}
213
+ return
214
+
215
+ @property
216
+ def info(self) -> str:
217
+ return f"{{{self.name}}} {self.arg}"
218
+
219
+ @property
220
+ def _md(self) -> str:
221
+ options = []
222
+ for key, value in self.options.items():
223
+ val_str = str(value) if value is not None else ""
224
+ if "\n" in val_str:
225
+ val_content = "\n".join(f"{' ' * 4}{line}" for line in val_str.split("\n"))
226
+ val_str = f"|\n{val_content}"
227
+ options.append(f":{key}: {val_str}")
228
+ options = "\n".join(options)
229
+ options_section = f"{options}\n\n" if options else ""
230
+ return f"{self._start_line}\n{options_section}${{content}}\n{self._end_line}"
231
+
232
+
233
+ def thematic_break(char: _Literal["-", "_", "*"] = "-") -> ThematicBreak:
234
+ """Create a [thematic break](https://github.github.com/gfm/#thematic-break).
235
+
236
+ Parameters
237
+ ----------
238
+ char : {'*', '_', '-'}, default: '-'
239
+ Thematic break character.
240
+ """
241
+ return ThematicBreak(char=char)
242
+
243
+
244
+ def heading(level: _Literal[1, 2, 3, 4, 5, 6], content: _ElementContentInputType = "") -> ATXHeading:
245
+ """Create an ATX heading.
246
+
247
+ Parameters
248
+ ----------
249
+ level : {1, 2, 3, 4, 5, 6}
250
+ Heading level.
251
+ content : str
252
+ Heading content.
253
+ """
254
+ return ATXHeading(level=level, contents=content)
255
+
256
+
257
+ def field_list_element(
258
+ name: _ElementContentType,
259
+ body: _ElementContentInputType = "",
260
+ indent_size: int = 4,
261
+ ) -> FieldListElement:
262
+ """Create a field list element.
263
+
264
+ Parameters
265
+ ----------
266
+ name : ElementContentType
267
+ Field name.
268
+ body : ElementContentInputType, optional
269
+ Field body.
270
+ indent_size : int, default: 4
271
+ Indent size.
272
+ """
273
+ return FieldListElement(name=name, body=body, indent_size=indent_size)
274
+
275
+
276
+ def field_list(
277
+ elements: list[FieldListElement | tuple[_ElementContentType, _ElementContentInputType]],
278
+ indent_size: int = 4,
279
+ ) -> FieldList:
280
+ """Create a field list.
281
+
282
+ Parameters
283
+ ----------
284
+ elements : list[FieldListElement]
285
+ Field list elements.
286
+ indent_size : int, default: 4
287
+ Indent size.
288
+ """
289
+ elements = [
290
+ elem if isinstance(elem, FieldListElement) else field_list_element(name=elem[0], body=elem[1])
291
+ for elem in elements
292
+ ]
293
+ return FieldList(elements=elements, indent_size=indent_size)
294
+
295
+
296
+ def html_block(content: _ElementContentInputType = None) -> HTMLBlock:
297
+ """Create an [HTML block](https://github.github.com/gfm/#html-block).
298
+
299
+ Parameters
300
+ ----------
301
+ content : ElementContentInputType, optional
302
+ HTML content.
303
+ """
304
+ return HTMLBlock(contents=content)
305
+
306
+ def code_fence(
307
+ content: _ElementContentInputType = None,
308
+ info: _Stringable = "",
309
+ fence: _Literal["`", "~", ":"] = "`",
310
+ ) -> CodeFence:
311
+ """Create a [fenced code block](https://github.github.com/gfm/#fenced-code-block).
312
+
313
+ Parameters
314
+ ----------
315
+ content : ElementContentInputType, optional
316
+ Code block content.
317
+ info : Stringable, optional
318
+ Code block [info string](https://github.github.com/gfm/#info-string).
319
+ fence: {'`', '~', ':'}, default: '`'
320
+ Fence character.
321
+ """
322
+ return CodeFence(contents=content, info=info, fence=fence)
323
+
324
+
325
+ def directive(
326
+ name: _Stringable,
327
+ content: _ElementContentInputType = None,
328
+ arg: _Stringable = "",
329
+ options: dict[_Stringable, _Stringable] | None = None,
330
+ fence: _Literal["`", "~", ":"] = "`",
331
+ ) -> Directive:
332
+ """Create a directive.
333
+
334
+ Parameters
335
+ ----------
336
+ name : Stringable
337
+ Directive name.
338
+ content : ElementContentInputType, optional
339
+ Directive content.
340
+ arg : Stringable, optional
341
+ Directive argument.
342
+ options : dict[Stringable, Stringable], optional
343
+ Directive options.
344
+ fence: {'`', '~', ':'}, default: '`'
345
+ Fence character.
346
+ """
347
+ return Directive(name=name, contents=content, arg=arg, options=options, fence=fence)
348
+
349
+
350
+ def admonition(
351
+ title: _ElementContentType,
352
+ content: _ElementContentInputType,
353
+ class_: str | list[str] | None = None,
354
+ name: _Stringable | None = None,
355
+ fence: _Literal["`", "~", ":"] = "`",
356
+ ) -> Directive:
357
+ """Create a [MyST admonition](https://myst-parser.readthedocs.io/en/latest/syntax/admonitions.html).
358
+
359
+ Parameters
360
+ ----------
361
+ title : ElementContentType
362
+ Admonition title.
363
+ content : ElementContentInputType
364
+ Admonition content.
365
+ class_ : str | list[str], optional
366
+ CSS class names to add to the admonition. These must conform to the
367
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
368
+ name : Stringable, optional
369
+ A reference target name for the admonition
370
+ (for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
371
+ fence: {'`', '~', ':'}, default: '`'
372
+ Fence character.
373
+ """
374
+ options = process_directive_options({"class": class_, "name": name})
375
+ return Directive(name="admonition", contents=content, arg=title, options=options, fence=fence)
376
+
377
+
378
+ def code_block(
379
+ language: str | None,
380
+ content: _ElementContentType,
381
+ caption: _ElementContentType | None = None,
382
+ class_: str | list[str] | None = None,
383
+ name: _Stringable | None = None,
384
+ lineno_start: int | None = None,
385
+ emphasize_lines: list[int] | None = None,
386
+ force: bool = False,
387
+ fence: _Literal["`", "~", ":"] = "`",
388
+ ):
389
+ """Create a MyST [code block directive](https://myst-parser.readthedocs.io/en/latest/syntax/code_and_apis.html#adding-a-caption).
390
+
391
+ Parameters
392
+ ----------
393
+ language : str, optional
394
+ Language of the code, e.g. 'python', 'json', 'bash', 'html'.
395
+ content : ElementContentType
396
+ Code to be included in the code block.
397
+ caption : ElementContentType, optional
398
+ Caption for the code block.
399
+ class_ : list[str], optional
400
+ CSS class names to add to the code block. These must conform to the
401
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
402
+ name : Stringable, optional
403
+ A reference target name for the code block
404
+ (for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
405
+ lineno_start : int, optional
406
+ Starting line number for the code block.
407
+ emphasize_lines : list[int], optional
408
+ Line numbers to highlight in the code block.
409
+ Note that `lineno-start` must be set for this to work.
410
+ force : bool, default: False
411
+ Allow minor errors on highlighting to be ignored.
412
+ fence: {'`', '~', ':'}, default: '`'
413
+ Fence character.
414
+ """
415
+ options = process_directive_options(
416
+ {k: v for k, v in locals() if k not in ("language", "content", "fence")}
417
+ )
418
+ return Directive(name="code-block", contents=content, arg=language, options=options, fence=fence)
419
+
420
+
421
+ def tab_item(
422
+ title: _Stringable,
423
+ content: _ElementContentInputType,
424
+ selected: bool = False,
425
+ name: _Stringable | None = None,
426
+ sync: _Stringable | None = None,
427
+ class_container: str | list[str] | None = None,
428
+ class_label: str | list[str] | None = None,
429
+ class_content: str | list[str] | None = None,
430
+ fence: _Literal["`", "~", ":"] = "`",
431
+ ) -> Directive:
432
+ """Create a [Sphinx-Design tab item](https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html).
433
+
434
+ Parameters
435
+ ----------
436
+ title : Stringable
437
+ Tab title.
438
+ content : ElementContentInputType
439
+ Tab content.
440
+ selected : bool, default: False
441
+ Whether the tab item is selected by default.
442
+ name : Stringable, optional
443
+ A reference target name for the tab item
444
+ (for [cross-referencing](https://myst-parser.readthedocs.io/en/latest/syntax/cross-referencing.html#syntax-referencing)).
445
+ sync : Stringable, optional
446
+ A key that is used to sync the selected tab across multiple tab-sets.
447
+ class_container : str | list[str], optional
448
+ CSS class names to add to the container element. These must conform to the
449
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
450
+ class_label : str | list[str], optional
451
+ CSS class names to add to the label element. These must conform to the
452
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
453
+ class_content : str | list[str], optional
454
+ CSS class names to add to the content element. These must conform to the
455
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
456
+ fence: {'`', '~', ':'}, default: '`'
457
+ Fence character.
458
+ """
459
+ options = process_directive_options(
460
+ {k: v for k, v in locals() if k not in ("title", "content", "fence")}
461
+ )
462
+ return Directive(name="tab-item", contents=content, arg=title, options=options, fence=fence)
463
+
464
+
465
+ def tab_set(
466
+ content: list[Directive],
467
+ class_: list[str] | None = None,
468
+ sync_group: _Stringable | None = None,
469
+ fence: _Literal["`", "~", ":"] = "`",
470
+ ) -> Directive:
471
+ """Create a [Sphinx-Design tab set](https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html).
472
+
473
+ Parameters
474
+ ----------
475
+ content : list[Directive]
476
+ Tab items.
477
+ class_ : list[str], optional
478
+ CSS class names to add to the tab set. These must conform to the
479
+ [identifier normalization rules](https://docutils.sourceforge.io/docs/ref/rst/directives.html#identifier-normalization).
480
+ sync_group : Stringable, optional
481
+ Group name for synchronized tab sets.
482
+ fence: {'`', '~', ':'}, default: '`'
483
+ Fence character.
484
+ """
485
+ options = process_directive_options(
486
+ {k: v for k, v in locals() if k not in ("content", "fence")}
487
+ )
488
+ return Directive(name="tab-set", contents=content, options=options, fence=fence)
489
+
490
+
491
+ def card(
492
+ header_content: _ElementContentInputType = None,
493
+ body_content: _ElementContentInputType = None,
494
+ footer_content: _ElementContentInputType = None,
495
+ body_title: _Stringable = "",
496
+ width: _Literal["auto"] | int | None = None,
497
+ margin: _Literal["auto", 0, 1, 2, 3, 4, 5] | tuple[_Literal["auto", 0, 1, 2, 3, 4, 5], ...] | None = None,
498
+ text_align: _Literal["left", "center", "right", "justify"] | None = None,
499
+ img_background: _Stringable | None = None,
500
+ img_top: _Stringable | None = None,
501
+ img_bottom: _Stringable | None = None,
502
+ img_alt: _Stringable | None = None,
503
+ link: _Stringable | None = None,
504
+ link_type: _Literal["url", "ref", "doc", "any"] | None = None,
505
+ link_alt: _Stringable | None = None,
506
+ shadow: _Literal["sm", "md", "lg", "none"] | None = None,
507
+ class_card: list[str] | None = None,
508
+ class_header: list[str] | None = None,
509
+ class_body: list[str] | None = None,
510
+ class_footer: list[str] | None = None,
511
+ class_title: list[str] | None = None,
512
+ class_img_top: list[str] | None = None,
513
+ class_img_bottom: list[str] | None = None,
514
+ fence: _Literal["`", "~", ":"] = "`",
515
+ ) -> Directive:
516
+
517
+ def process_content(content, key_prefix: str):
518
+ if isinstance(content, (list, tuple)):
519
+ return {f"{key_prefix}_{idx}": elem for idx, elem in enumerate(content)}
520
+ if not isinstance(content, dict):
521
+ return {key_prefix: content}
522
+ return content
523
+
524
+ options = process_directive_options(
525
+ {
526
+ k: v for k, v in locals() if k not in (
527
+ "header_content", "body_content", "footer_content", "body_title", "fence"
528
+ )
529
+ }
530
+ )
531
+ content = {}
532
+ if header_content:
533
+ for header_id, header in process_content(header_content, "header"):
534
+ content[header_id] = header
535
+ content["header_body_separator"] = "^^^"
536
+ if body_content:
537
+ for body_id, body in process_content(body_content, "body"):
538
+ content[body_id] = body
539
+ if footer_content:
540
+ content["body_footer_separator"] = "+++"
541
+ for footer_id, footer in process_content(footer_content, "footer"):
542
+ content[footer_id] = footer
543
+ return Directive(name="card", contents=content, arg=body_title, options=options, fence=fence)
544
+
545
+
546
+ def process_directive_options(options: dict) -> dict:
547
+ final_options = {}
548
+ for key, val in options.items():
549
+ if val is None or val is False:
550
+ continue
551
+ if isinstance(val, (list, tuple)):
552
+ val = " ".join([str(e) for e in val])
553
+ elif isinstance(val, bool):
554
+ val = ""
555
+ key_name = str(key).removesuffix("_").replace("_", "-")
556
+ final_options[key_name] = val
557
+ return final_options
558
+
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+ import re as _re
3
+
4
+ import pyserials as _ps
5
+
6
+
7
+ def frontmatter(file_content: str) -> dict:
8
+ match = _re.match(r'^---+\s*\n(.*?)(?=\n---+\s*(\n|$))', file_content, _re.DOTALL)
9
+ if not match:
10
+ return {}
11
+ frontmatter_text = match.group(1).strip()
12
+ frontmatter_dict = _ps.read.yaml_from_string(frontmatter_text)
13
+ return frontmatter_dict
14
+
15
+
16
+ def title(file_content: str) -> str | None:
17
+ match = _re.search(r"^# (.*)", file_content, _re.MULTILINE)
18
+ return match.group(1) if match else ""
19
+
20
+
21
+ def toctree(file_content: str) -> tuple[str, ...] | None:
22
+ matches = _re.findall(r"(:{3,}){toctree}\s((.|\s)*?)\s\1", file_content, _re.DOTALL)
23
+ if not matches:
24
+ return
25
+ toctree_str = matches[0][1]
26
+ toctree_entries = []
27
+ for line in toctree_str.splitlines():
28
+ entry = line.strip()
29
+ if entry and not entry.startswith(":"):
30
+ toctree_entries.append(entry)
31
+ return tuple(toctree_entries)
@@ -0,0 +1,396 @@
1
+ from typing import Literal as _Literal, Callable as _Callable
2
+ from functools import partial as _partial
3
+ import cmarkgfm as _gfm_pypi
4
+ from readme_renderer import markdown as _readme_renderer_md, clean as _readme_renderer_clean
5
+
6
+
7
+
8
+
9
+ import markdown_it as _mdit
10
+ import markdown_it.utils as _mdit_utils
11
+
12
+ from mdit_py_plugins.amsmath import amsmath_plugin as _mdit_plugin_amsmath
13
+ from mdit_py_plugins.anchors import anchors_plugin as _mdit_plugin_anchors
14
+ from mdit_py_plugins.attrs import attrs_block_plugin as _mdit_plugin_attrs_block, attrs_plugin as _mdit_plugin_attrs
15
+ from mdit_py_plugins.colon_fence import colon_fence_plugin as _mdit_plugin_colon_fence
16
+ from mdit_py_plugins.deflist import deflist_plugin as _mdit_plugin_deflist
17
+ from mdit_py_plugins.dollarmath import dollarmath_plugin as _mdit_plugin_dollarmath
18
+ from mdit_py_plugins.field_list import fieldlist_plugin as _mdit_plugin_fieldlist
19
+ from mdit_py_plugins.footnote import footnote_plugin as _mdit_plugin_footnote
20
+ from mdit_py_plugins.front_matter import front_matter_plugin as _mdit_plugin_front_matter
21
+ from mdit_py_plugins.myst_blocks import myst_block_plugin as _mdit_plugin_myst_block
22
+ from mdit_py_plugins.myst_role import myst_role_plugin as _mdit_plugin_myst_role
23
+ from mdit_py_plugins.substitution import substitution_plugin as _mdit_plugin_substitution
24
+ from mdit_py_plugins.tasklists import tasklists_plugin as _mdit_plugin_tasklists
25
+ from mdit_py_plugins.wordcount import wordcount_plugin as _mdit_plugin_wordcount
26
+
27
+
28
+ # from myst_parser.config.main import MdParserConfig
29
+
30
+
31
+ def to_html(
32
+ source: str,
33
+ components_core: set[
34
+ _Literal['block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join']
35
+ ] = ('block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join'),
36
+ components_block: set[
37
+ _Literal[
38
+ 'blockquote',
39
+ 'code',
40
+ 'fence',
41
+ 'heading',
42
+ 'hr',
43
+ 'html_block',
44
+ 'lheading',
45
+ 'list',
46
+ 'paragraph',
47
+ 'reference',
48
+ 'table',
49
+ ]
50
+ ] = (
51
+ 'blockquote',
52
+ 'code',
53
+ 'fence',
54
+ 'heading',
55
+ 'hr',
56
+ 'html_block',
57
+ 'lheading',
58
+ 'list',
59
+ 'paragraph',
60
+ 'reference',
61
+ 'table',
62
+ ),
63
+ components_inline: set[
64
+ _Literal[
65
+ 'autolink',
66
+ 'backticks',
67
+ 'balance_pairs',
68
+ 'emphasis',
69
+ 'entity',
70
+ 'escape',
71
+ 'fragments_join',
72
+ 'html_inline',
73
+ 'image',
74
+ 'link',
75
+ 'linkify',
76
+ 'newline',
77
+ 'strikethrough',
78
+ 'text'
79
+ ]
80
+ ] = (
81
+ 'autolink',
82
+ 'backticks',
83
+ 'balance_pairs',
84
+ 'emphasis',
85
+ 'entity',
86
+ 'escape',
87
+ 'fragments_join',
88
+ 'html_inline',
89
+ 'image',
90
+ 'link',
91
+ 'linkify',
92
+ 'newline',
93
+ 'strikethrough',
94
+ 'text'
95
+ ),
96
+ plugins: set[_Callable | tuple[_Callable, dict]] = (
97
+ _partial(_mdit_plugin_amsmath, renderer=None),
98
+ _partial(
99
+ _mdit_plugin_anchors,
100
+ min_level=1,
101
+ max_level=6,
102
+ slug_func=None,
103
+ permalink=True,
104
+ permalinkSymbol='¶',
105
+ permalinkBefore=False,
106
+ permalinkSpace=True,
107
+ ),
108
+ _partial(
109
+ _mdit_plugin_attrs,
110
+ after=('image', 'code_inline', 'link_close', 'span_close'),
111
+ spans=True,
112
+ span_after='link',
113
+ ),
114
+ _mdit_plugin_attrs_block,
115
+ _mdit_plugin_colon_fence,
116
+ _mdit_plugin_deflist,
117
+ _partial(
118
+ _mdit_plugin_dollarmath,
119
+ allow_labels=True,
120
+ allow_space=True,
121
+ allow_digits=True,
122
+ allow_blank_lines=True,
123
+ double_inline=False,
124
+ ),
125
+ _mdit_plugin_fieldlist,
126
+ _partial(_mdit_plugin_footnote, inline=True, move_to_end=True, always_match_refs=True),
127
+ _mdit_plugin_front_matter,
128
+ _mdit_plugin_myst_block,
129
+ _mdit_plugin_myst_role,
130
+ _partial(_mdit_plugin_substitution, start_delimiter='{', end_delimiter='}'),
131
+ _partial(_mdit_plugin_tasklists, enabled=False, label=False, label_after=False),
132
+ _partial(_mdit_plugin_wordcount, per_minute=200, store_text=False),
133
+ ),
134
+ env: dict | None = None,
135
+ html: bool = True,
136
+ linkify: bool = True,
137
+ linkify_fuzzy_links: bool = True,
138
+ typographer: bool = True,
139
+ quotes: str = '“”‘’',
140
+ xhtml_out: bool = True,
141
+ breaks: bool = True,
142
+ lang_prefix: str = 'language-',
143
+ highlight: _Callable[[str, str, str], str] = None,
144
+ ):
145
+ """Convert Markdown to HTML using the [`markdown-it-py`](https://markdown-it-py.readthedocs.io/) library.
146
+
147
+ Parameters
148
+ ----------
149
+ source : str
150
+ Markdown source text to convert to HTML.
151
+ components_core : set of {'block', 'inline', 'linkify', 'normalize', 'replacements', 'smartquotes', 'text_join'}
152
+ Enabled core components
153
+ (cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_core)).
154
+ components_block : set of {'blockquote', 'code', 'fence', 'heading', 'hr', 'html_block', 'lheading', 'list', 'paragraph', 'reference', 'table'}
155
+ Enabled block components
156
+ (cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_block)).
157
+ components_inline : set of {'autolink', 'backticks', 'emphasis', 'entity', 'escape', 'html_inline', 'image', 'link', 'linkify', 'newline', 'strikethrough', 'text'}
158
+ Enabled inline components
159
+ (cf. [`markdown-it-py` source code](https://github.com/executablebooks/markdown-it-py/tree/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/rules_inline)).
160
+ plugins : set of Callable[[MarkdownIt], None] or tuple[Callable[[MarkdownIt], None], dict], default: (front_matter_plugin,)
161
+ List of plugins to apply to the parser.
162
+ Each entry can either be a callable, or a tuple of a callable and a dictionary of keyword arguments.
163
+ The callable should take as its first argument the `MarkdownIt` parser instance,
164
+ followed by any additional arguments.
165
+ By default, all plugins from the [`mdit_py_plugins` library](https://mdit-py-plugins.readthedocs.io)
166
+ (cf. [source code](https://github.com/executablebooks/mdit-py-plugins/tree/d11bdaf0979e6fae01c35db5a4d1f6a4b4dd8843/mdit_py_plugins))
167
+ except for `admon_plugin`, `container_plugin`, and `texmath_plugin`
168
+ are enabled with their default configurations.
169
+ env : dict, optional
170
+ Environment variables to pass to the parser.
171
+ It is used to pass data between “distributed” rules and return additional metadata
172
+ like reference info, needed for the renderer.
173
+ It can also be used to inject data, e.g., when using the `substitution_plugin`.
174
+ html : bool, default: True
175
+ Allow raw HTML tags in the source text.
176
+ linkify : bool, default: True
177
+ Automatically convert URL-like text to links using the
178
+ [`linkify-it-py`](https://github.com/tsutsu3/linkify-it-py) library.
179
+ linkify_fuzzy_links : bool, default: True
180
+ Enable fuzzy link detection for `linkify`.
181
+ This allows URLs without a protocol schema (e.g., `repodynamics.com`) to be detected as links.
182
+ typographer : bool, default: True
183
+ Enable smartquotes and replacements.
184
+ This will automatically add the `smartquotes` and `replacements` core components as well.
185
+ quotes : str, default: '“”‘’'
186
+ Quote characters.
187
+ xhtml_out : bool, default: True
188
+ Use '/' to close single tags (e.g., `<br />`).
189
+ breaks : bool, default: True
190
+ Convert newlines in paragraphs into `<br>` tags.
191
+ lang_prefix : str, default: 'language-'
192
+ CSS language prefix for fenced blocks.
193
+ highlight: Callable[[str, str, str], str] or None, default: None
194
+ An optional highlighter function `f(content, language, attributes) -> str`
195
+ to apply syntax highlighting to code blocks.
196
+
197
+ References
198
+ ----------
199
+ - [`markdown-it` Parser options](https://markdown-it-py.readthedocs.io/en/latest/api/markdown_it.utils.html#markdown_it.utils.OptionsType)
200
+ """
201
+ options = _mdit_utils.OptionsType(
202
+ maxNesting=50,
203
+ html=html,
204
+ linkify=linkify,
205
+ typographer=typographer,
206
+ quotes=quotes,
207
+ xhtmlOut=xhtml_out,
208
+ breaks=breaks,
209
+ langPrefix=lang_prefix,
210
+ highlight=highlight,
211
+ )
212
+ inline_rules = []
213
+ inline_rules2 = []
214
+ # For some reason (?!), `markdown-it-py` has 'inline' and 'inline2' rules.
215
+ # Enabling 'emphasis' and 'strikethrough' adds them to both 'inline' and 'inline2',
216
+ # while enabling 'balance_pairs' and 'fragments_join' only adds them to 'inline2'.
217
+ # All other components are only added to 'inline'.
218
+ # See: https://markdown-it-py.readthedocs.io/en/latest/using.html#the-parser
219
+ # Code: https://github.com/executablebooks/markdown-it-py/blob/c10312e2e475a22edb92abede15d3dcabd0cac0c/markdown_it/parser_inline.py#L38-L51
220
+ # For simplicity we have merged 'inline' and 'inline2' inputs into the 'components_inline' parameter.
221
+ # Now we need to separate them again.
222
+ for rule in components_inline:
223
+ if rule in ("emphasis", "strikethrough"):
224
+ inline_rules.append(rule)
225
+ inline_rules2.append(rule)
226
+ elif rule in ("balance_pairs", "fragments_join"):
227
+ inline_rules2.append(rule)
228
+ else:
229
+ inline_rules.append(rule)
230
+ components = {
231
+ "core": {"rules": list(components_core)},
232
+ "block": {"rules": list(components_block)},
233
+ "inline": {"rules": inline_rules, "rules2": inline_rules2},
234
+ }
235
+ config = _mdit_utils.PresetType(options=options, components=components)
236
+ parser = _mdit.MarkdownIt(config=config)
237
+ if typographer:
238
+ parser.enable(["replacements", "smartquotes"])
239
+ if parser.linkify is not None:
240
+ parser.linkify.set({"fuzzy_link": linkify_fuzzy_links})
241
+ for plugin in plugins:
242
+ if isinstance(plugin, (list, tuple)):
243
+ plugin_func, plugin_config = plugin
244
+ else:
245
+ plugin_func = plugin
246
+ plugin_config = {}
247
+ parser.use(plugin_func, **plugin_config)
248
+ return parser.render(src=source, env=env)
249
+
250
+
251
+
252
+ # def create_md_parser(
253
+ # config: MdParserConfig, renderer: Callable[[MarkdownIt], RendererProtocol]
254
+ # ) -> MarkdownIt:
255
+ # """Return a Markdown parser with the required MyST configuration."""
256
+ #
257
+ # md.options.update(
258
+ # {
259
+ # "typographer": typographer,
260
+ # "linkify": "linkify" in config.enable_extensions,
261
+ # "myst_config": config,
262
+ # }
263
+ # )
264
+ #
265
+ # return md
266
+
267
+
268
+ def gfm_to_html_pypi(
269
+ source: str,
270
+ extensions: tuple[str, ...] = ('autolink', 'strikethrough', 'table', 'tagfilter', 'tasklist'),
271
+ unsafe: bool = True,
272
+ smart: bool = False,
273
+ normalize: bool = False,
274
+ hard_breaks: bool = False,
275
+ no_breaks: bool = False,
276
+ source_pos: bool = False,
277
+ footnotes: bool = False,
278
+ validate_utf8: bool = False,
279
+ github_pre_lang: bool = True,
280
+ liberal_html_tag: bool = False,
281
+ strikethrough_double_tilde: bool = False,
282
+ table_prefer_style_attributes: bool = False,
283
+ highlight_code: bool = True,
284
+ sanitize: bool = True,
285
+ ):
286
+ """Convert CommonMark or GitHub Flavored Markdown to HTML
287
+ using the [CMarkGFM](https://github.com/theacodes/cmarkgfm) library.
288
+
289
+ CMarkGFM is the Markdown to HTML converter
290
+ used by the Python Packaging Authority (PyPA)'s
291
+ [`readme_renderer`](https://github.com/pypa/readme_renderer) library to render
292
+ [package READMEs on PYPI](https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme).
293
+ Using this function with the default arguments will exactly replicate the rendering
294
+ used by PyPI.
295
+
296
+ Parameters
297
+ ----------
298
+ source : str
299
+ GitHub Flavored Markdown source text to convert to HTML.
300
+ extensions : Sequence[str], default: ('autolink', 'strikethrough', 'table', 'tagfilter', 'tasklist')
301
+ List of extensions to enable on top of the CommonMark specifications.
302
+ The default value enables all GitHub Flavored Markdown extensions,
303
+ which are currently the only [available extensions in CMarkGFM](https://github.com/theacodes/cmarkgfm/blob/66b131cee950ad30cad9dfbf7f2360270ed105b8/src/cmarkgfm/cmark.py#L118C13-L118C74).
304
+ unsafe: bool, default: True
305
+ Allow rendering unsafe HTML (e.g., `<script>` elements)
306
+ and URLs (e.g., those starting with `javascript:`, `vbscript:`, `file:`, or `data:` (except for `image/png`, `image/gif`, `image/jpeg`, or `image/webp` media types)).
307
+ If set to False, raw HTML is replaced by a placeholder comment and
308
+ potentially dangerous URLs are replaced by an empty string.
309
+ smart: bool, default: False
310
+ Render smart punctuation.
311
+ This is roughly equivalent to the `smartquotes` and `replacements` typographic components
312
+ in [`markdown-it`](https://markdown-it-py.readthedocs.io/en/latest/using.html#typographic-components),
313
+ e.g., converting basic quote marks to their opening and closing variants, and `--` and `---`
314
+ to en-dash `–` and em-dash `—`, respectively.
315
+ normalize: bool, default: False
316
+ Consolidate adjacent text nodes.
317
+ hard_breaks: bool, default: False
318
+ Render line breaks within paragraphs as `<br>` tags.
319
+ no_breaks: bool, default: False
320
+ Render soft line breaks as spaces.
321
+ source_pos: bool, default: False
322
+ Add attribute `data-sourcepos` to HTML tags
323
+ indicating the corresponding line/column ranges in the input.
324
+ footnotes: bool, default: False
325
+ Parse footnotes.
326
+ validate_utf8: bool, default: False
327
+ Validate UTF-8 in the input before parsing,
328
+ replacing illegal sequences with the replacement character `U+FFFD`.
329
+ github_pre_lang: bool, default: True
330
+ Use GitHub style for indicating the language of code blocks.
331
+ If True (default), the code block's language defined in its info string will be used
332
+ as the value of the `lang` attribute of the `<pre>` element
333
+ (e.g., `<pre lang="python"><code>...</code></pre>`),
334
+ otherwise it will be used as the value of the `class` attribute of the `<code>` element
335
+ according to [highlight.js](https://highlightjs.org/) style
336
+ (e.g., `<pre><code class="language-python">...</code></pre>`).
337
+ liberal_html_tag: bool, default: False
338
+ Be liberal in interpreting inline HTML tags.
339
+ strikethrough_double_tilde: bool, default: False
340
+ Only parse strikethroughs if surrounded by exactly 2 tildes.
341
+ Gives some compatibility with redcarpet.
342
+ table_prefer_style_attributes: bool, default: False
343
+ Use style attributes to align table cells instead of align attributes.
344
+ highlight_code : bool, default: True
345
+ Apply syntax highlighting to code blocks using the [`Pygments`](https://pygments.org/) library.
346
+ This exactly replicates the rendering used in PyPI.
347
+ However, notice that `readme_renderer` uses a naive RegEx to detect `<pre>` HTML elements.
348
+ Thus, this may not work on custom-written `<pre>` elements
349
+ (i.e., those not generated from Markdown by CMarkGFM in the previous step).
350
+ sanitize : bool, default: True
351
+ Sanitize the HTML output using the [`nh3`](https://nh3.readthedocs.io/en/latest/)
352
+ library to remove potentially dangerous content.
353
+ PyPI uses this to prevent XSS attacks by allowing only a
354
+ [subset of HTML tags](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/clean.py#L20-L31)
355
+ and [attributes](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/clean.py#L33-L65).
356
+
357
+ Notes
358
+ -----
359
+ - [`twine check`](https://twine.readthedocs.io/en/stable/#twine-check) only works for
360
+ reStructuredText (reST) READMEs; it always passes for Markdown content
361
+ (cf. [`twine.commands.check._RENDERERS`](https://github.com/pypa/twine/blob/4f7cd66fa1ceba7f8de5230d3d4ebea0787f17e5/twine/commands/check.py#L32-L37))
362
+ and thus cannot be used to validate Markdown.
363
+
364
+ References
365
+ ----------
366
+ - [`cmarkgfm.cmark` module](https://github.com/theacodes/cmarkgfm/blob/66b131cee950ad30cad9dfbf7f2360270ed105b8/src/cmarkgfm/cmark.py)
367
+ - [`readme_renderer.markdown` module](https://github.com/pypa/readme_renderer/blob/1d0497c37a6033d791c74e800590dcd0d34f6e08/readme_renderer/markdown.py)
368
+ """
369
+ options = 0
370
+ for arg, cmark_arg in (
371
+ (unsafe, _gfm_pypi.Options.CMARK_OPT_UNSAFE),
372
+ (smart, _gfm_pypi.Options.CMARK_OPT_SMART),
373
+ (normalize, _gfm_pypi.Options.CMARK_OPT_NORMALIZE),
374
+ (hard_breaks, _gfm_pypi.Options.CMARK_OPT_HARDBREAKS),
375
+ (no_breaks, _gfm_pypi.Options.CMARK_OPT_NOBREAKS),
376
+ (source_pos, _gfm_pypi.Options.CMARK_OPT_SOURCEPOS),
377
+ (footnotes, _gfm_pypi.Options.CMARK_OPT_FOOTNOTES),
378
+ (validate_utf8, _gfm_pypi.Options.CMARK_OPT_VALIDATE_UTF8),
379
+ (github_pre_lang, _gfm_pypi.Options.CMARK_OPT_GITHUB_PRE_LANG),
380
+ (liberal_html_tag, _gfm_pypi.Options.CMARK_OPT_LIBERAL_HTML_TAG),
381
+ (strikethrough_double_tilde, _gfm_pypi.Options.CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE),
382
+ (table_prefer_style_attributes, _gfm_pypi.Options.CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES),
383
+ ):
384
+ if arg:
385
+ options |= cmark_arg
386
+
387
+ html_syntax: str = _gfm_pypi.markdown_to_html_with_extensions(
388
+ text=source,
389
+ options=options,
390
+ extensions=extensions,
391
+ )
392
+ if highlight_code:
393
+ html_syntax = _readme_renderer_md._highlight(html_syntax)
394
+ if sanitize:
395
+ html_syntax = _readme_renderer_clean.clean(html_syntax)
396
+ return html_syntax