cmark-render 0.1.0__tar.gz → 0.2.1__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.
- cmark_render-0.2.1/PKG-INFO +105 -0
- cmark_render-0.2.1/README.md +86 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/pyproject.toml +7 -1
- cmark_render-0.2.1/src/cmark_render/__init__.py +36 -0
- cmark_render-0.2.1/src/cmark_render/_cmark.c +219 -0
- cmark_render-0.2.1/src/cmark_render/_cmark.pyi +18 -0
- cmark_render-0.2.1/src/cmark_render.egg-info/PKG-INFO +105 -0
- cmark_render-0.2.1/tests/test_render.py +197 -0
- cmark_render-0.1.0/PKG-INFO +0 -65
- cmark_render-0.1.0/README.md +0 -46
- cmark_render-0.1.0/src/cmark_render/__init__.py +0 -5
- cmark_render-0.1.0/src/cmark_render/_cmark.c +0 -122
- cmark_render-0.1.0/src/cmark_render/_cmark.pyi +0 -1
- cmark_render-0.1.0/src/cmark_render.egg-info/PKG-INFO +0 -65
- cmark_render-0.1.0/tests/test_render.py +0 -108
- {cmark_render-0.1.0 → cmark_render-0.2.1}/LICENSE +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/MANIFEST.in +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/setup.cfg +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/setup.py +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render/py.typed +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/SOURCES.txt +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/dependency_links.txt +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/top_level.txt +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/COPYING +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/UPSTREAM +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/autolink.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/autolink.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/cmark-gfm-core-extensions.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/core-extensions.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/ext_scanners.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/ext_scanners.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/strikethrough.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/strikethrough.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/table.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/table.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tagfilter.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tagfilter.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tasklist.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tasklist.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/cmark-gfm_export.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/cmark-gfm_version.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/config.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/arena.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/blocks.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/buffer.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/buffer.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/case_fold_switch.inc +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/chunk.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark-gfm-extension_api.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark-gfm.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark_ctype.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark_ctype.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/commonmark.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/entities.inc +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/footnotes.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/footnotes.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_href_e.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_html_e.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_html_u.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/html.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/html.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/inlines.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/inlines.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/iterator.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/iterator.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/latex.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/linked_list.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/man.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/map.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/map.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/node.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/node.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/parser.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plaintext.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plugin.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plugin.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/references.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/references.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/registry.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/registry.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/render.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/render.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/scanners.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/scanners.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/syntax_extension.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/syntax_extension.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/utf8.c +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/utf8.h +0 -0
- {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/xml.c +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmark-render
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Fast, leak-free markdown to HTML with a vendored cmark-gfm (tables, ~~strikethrough~~, raw HTML).
|
|
5
|
+
Author: Zachary Juang
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Source, https://github.com/zachary822/cmark-render
|
|
8
|
+
Keywords: markdown,commonmark,cmark,gfm
|
|
9
|
+
Classifier: Programming Language :: C
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
12
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: vendor/cmark-gfm/COPYING
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# cmark-render
|
|
21
|
+
|
|
22
|
+
Markdown to HTML in C, using a vendored, statically compiled
|
|
23
|
+
[cmark-gfm](https://github.com/github/cmark-gfm).
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from cmark_render import Extension, Option, render_html
|
|
27
|
+
|
|
28
|
+
render_html(
|
|
29
|
+
"| a | b |\n|---|---|\n| ~~old~~ | **new** |",
|
|
30
|
+
extensions=[Extension.TABLE, Extension.STRIKETHROUGH],
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
With no options or extensions, `render_html` is plain cmark-gfm: CommonMark
|
|
35
|
+
with its default options, so raw HTML and `javascript:`-style links are
|
|
36
|
+
dropped.
|
|
37
|
+
|
|
38
|
+
## Extensions
|
|
39
|
+
|
|
40
|
+
`extensions` takes the names of cmark-gfm's core syntax extensions, as
|
|
41
|
+
`Extension` members or plain strings. None are attached by default.
|
|
42
|
+
|
|
43
|
+
| `Extension` | Effect |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `TABLE` | GFM pipe tables. |
|
|
46
|
+
| `STRIKETHROUGH` | `~~text~~` and `~text~` become `<del>`. |
|
|
47
|
+
| `AUTOLINK` | Bare `www.` and `https://` URLs become links. |
|
|
48
|
+
| `TASKLIST` | `- [ ]` and `- [x]` list items become checkboxes. |
|
|
49
|
+
| `TAGFILTER` | Escapes `<script>`, `<iframe>`, `<style>` and other risky tags even under `UNSAFE`. |
|
|
50
|
+
|
|
51
|
+
An unknown name raises `ValueError`, and repeating a name has no extra effect.
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
`options` takes cmark-gfm's `CMARK_OPT_*` flags, as `Option` members combined
|
|
56
|
+
with `|`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
render_html(text, options=Option.SMART | Option.FOOTNOTES)
|
|
60
|
+
render_html(text, options=Option.UNSAFE) # keep raw HTML; sanitize untrusted input
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| `Option` | Effect |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `UNSAFE` | Pass raw HTML and `javascript:`-style links through. **Sanitize the output** if the input is untrusted. |
|
|
66
|
+
| `STRIKETHROUGH_DOUBLE_TILDE` | With `STRIKETHROUGH`, strike through only on `~~`, leaving single `~` alone. |
|
|
67
|
+
| `SMART` | Curly quotes, en and em dashes, ellipses. |
|
|
68
|
+
| `HARDBREAKS` | Render soft line breaks as `<br />`. |
|
|
69
|
+
| `NOBREAKS` | Render soft line breaks as spaces. |
|
|
70
|
+
| `SOURCEPOS` | Add `data-sourcepos` attributes to block elements. |
|
|
71
|
+
| `FOOTNOTES` | Parse `[^1]` footnotes. |
|
|
72
|
+
| `VALIDATE_UTF8` | Accepted, but has no effect: a `str` always arrives as valid UTF-8. |
|
|
73
|
+
| `GITHUB_PRE_LANG` | Write `<pre lang="x">` for fenced code instead of a `class` on `<code>`. |
|
|
74
|
+
| `FULL_INFO_STRING` | Keep the rest of a code fence's info string, in `data-meta`. |
|
|
75
|
+
| `LIBERAL_HTML_TAG` | Be looser about what counts as an inline HTML tag. |
|
|
76
|
+
| `TABLE_PREFER_STYLE_ATTRIBUTES` | With `TABLE`, align cells with `style` instead of `align`. |
|
|
77
|
+
|
|
78
|
+
An unknown bit raises `ValueError`.
|
|
79
|
+
|
|
80
|
+
## Memory and threads
|
|
81
|
+
|
|
82
|
+
Every render frees its syntax tree and rendered HTML, so memory stays flat
|
|
83
|
+
however many documents you render.
|
|
84
|
+
|
|
85
|
+
It is safe to use with threads and `fork()`. Renders run without the GIL.
|
|
86
|
+
Parsing runs one at a time, because cmark-gfm keeps a global table of special
|
|
87
|
+
characters that changes during each parse, but writing the HTML runs in
|
|
88
|
+
parallel. A `pthread_atfork` handler stops a
|
|
89
|
+
child process from inheriting a held lock.
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
pip install cmark-render
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Wheels are built for CPython 3.14 on macOS arm64 and manylinux x86_64 and
|
|
98
|
+
aarch64. On other platforms, pip builds from the sdist, which needs a C
|
|
99
|
+
compiler.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
BSD-2-Clause. The vendored cmark-gfm is under its own licenses, in
|
|
104
|
+
`vendor/cmark-gfm/COPYING`. `vendor/cmark-gfm/UPSTREAM` records the upstream
|
|
105
|
+
commit.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# cmark-render
|
|
2
|
+
|
|
3
|
+
Markdown to HTML in C, using a vendored, statically compiled
|
|
4
|
+
[cmark-gfm](https://github.com/github/cmark-gfm).
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from cmark_render import Extension, Option, render_html
|
|
8
|
+
|
|
9
|
+
render_html(
|
|
10
|
+
"| a | b |\n|---|---|\n| ~~old~~ | **new** |",
|
|
11
|
+
extensions=[Extension.TABLE, Extension.STRIKETHROUGH],
|
|
12
|
+
)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With no options or extensions, `render_html` is plain cmark-gfm: CommonMark
|
|
16
|
+
with its default options, so raw HTML and `javascript:`-style links are
|
|
17
|
+
dropped.
|
|
18
|
+
|
|
19
|
+
## Extensions
|
|
20
|
+
|
|
21
|
+
`extensions` takes the names of cmark-gfm's core syntax extensions, as
|
|
22
|
+
`Extension` members or plain strings. None are attached by default.
|
|
23
|
+
|
|
24
|
+
| `Extension` | Effect |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `TABLE` | GFM pipe tables. |
|
|
27
|
+
| `STRIKETHROUGH` | `~~text~~` and `~text~` become `<del>`. |
|
|
28
|
+
| `AUTOLINK` | Bare `www.` and `https://` URLs become links. |
|
|
29
|
+
| `TASKLIST` | `- [ ]` and `- [x]` list items become checkboxes. |
|
|
30
|
+
| `TAGFILTER` | Escapes `<script>`, `<iframe>`, `<style>` and other risky tags even under `UNSAFE`. |
|
|
31
|
+
|
|
32
|
+
An unknown name raises `ValueError`, and repeating a name has no extra effect.
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
`options` takes cmark-gfm's `CMARK_OPT_*` flags, as `Option` members combined
|
|
37
|
+
with `|`:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
render_html(text, options=Option.SMART | Option.FOOTNOTES)
|
|
41
|
+
render_html(text, options=Option.UNSAFE) # keep raw HTML; sanitize untrusted input
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| `Option` | Effect |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `UNSAFE` | Pass raw HTML and `javascript:`-style links through. **Sanitize the output** if the input is untrusted. |
|
|
47
|
+
| `STRIKETHROUGH_DOUBLE_TILDE` | With `STRIKETHROUGH`, strike through only on `~~`, leaving single `~` alone. |
|
|
48
|
+
| `SMART` | Curly quotes, en and em dashes, ellipses. |
|
|
49
|
+
| `HARDBREAKS` | Render soft line breaks as `<br />`. |
|
|
50
|
+
| `NOBREAKS` | Render soft line breaks as spaces. |
|
|
51
|
+
| `SOURCEPOS` | Add `data-sourcepos` attributes to block elements. |
|
|
52
|
+
| `FOOTNOTES` | Parse `[^1]` footnotes. |
|
|
53
|
+
| `VALIDATE_UTF8` | Accepted, but has no effect: a `str` always arrives as valid UTF-8. |
|
|
54
|
+
| `GITHUB_PRE_LANG` | Write `<pre lang="x">` for fenced code instead of a `class` on `<code>`. |
|
|
55
|
+
| `FULL_INFO_STRING` | Keep the rest of a code fence's info string, in `data-meta`. |
|
|
56
|
+
| `LIBERAL_HTML_TAG` | Be looser about what counts as an inline HTML tag. |
|
|
57
|
+
| `TABLE_PREFER_STYLE_ATTRIBUTES` | With `TABLE`, align cells with `style` instead of `align`. |
|
|
58
|
+
|
|
59
|
+
An unknown bit raises `ValueError`.
|
|
60
|
+
|
|
61
|
+
## Memory and threads
|
|
62
|
+
|
|
63
|
+
Every render frees its syntax tree and rendered HTML, so memory stays flat
|
|
64
|
+
however many documents you render.
|
|
65
|
+
|
|
66
|
+
It is safe to use with threads and `fork()`. Renders run without the GIL.
|
|
67
|
+
Parsing runs one at a time, because cmark-gfm keeps a global table of special
|
|
68
|
+
characters that changes during each parse, but writing the HTML runs in
|
|
69
|
+
parallel. A `pthread_atfork` handler stops a
|
|
70
|
+
child process from inheriting a held lock.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
pip install cmark-render
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Wheels are built for CPython 3.14 on macOS arm64 and manylinux x86_64 and
|
|
79
|
+
aarch64. On other platforms, pip builds from the sdist, which needs a C
|
|
80
|
+
compiler.
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
BSD-2-Clause. The vendored cmark-gfm is under its own licenses, in
|
|
85
|
+
`vendor/cmark-gfm/COPYING`. `vendor/cmark-gfm/UPSTREAM` records the upstream
|
|
86
|
+
commit.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cmark-render"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Fast, leak-free markdown to HTML with a vendored cmark-gfm (tables, ~~strikethrough~~, raw HTML)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.14"
|
|
@@ -29,6 +29,12 @@ cmark_render = ["py.typed", "*.pyi"]
|
|
|
29
29
|
[tool.setuptools.exclude-package-data]
|
|
30
30
|
cmark_render = ["*.c"]
|
|
31
31
|
|
|
32
|
+
[tool.ruff.lint]
|
|
33
|
+
extend-select = ["I", "B", "UP"]
|
|
34
|
+
|
|
35
|
+
[tool.mypy]
|
|
36
|
+
mypy_path = "src"
|
|
37
|
+
|
|
32
38
|
[tool.cibuildwheel]
|
|
33
39
|
build = "cp314-*"
|
|
34
40
|
# GIL builds on glibc only.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Fast, leak-free markdown to HTML with a vendored cmark-gfm."""
|
|
2
|
+
|
|
3
|
+
from enum import IntFlag, StrEnum
|
|
4
|
+
|
|
5
|
+
from cmark_render import _cmark
|
|
6
|
+
from cmark_render._cmark import render_html
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Option(IntFlag):
|
|
10
|
+
"""cmark-gfm's CMARK_OPT_* flags, for render_html's options."""
|
|
11
|
+
|
|
12
|
+
SOURCEPOS = _cmark.SOURCEPOS
|
|
13
|
+
HARDBREAKS = _cmark.HARDBREAKS
|
|
14
|
+
UNSAFE = _cmark.UNSAFE
|
|
15
|
+
NOBREAKS = _cmark.NOBREAKS
|
|
16
|
+
VALIDATE_UTF8 = _cmark.VALIDATE_UTF8
|
|
17
|
+
SMART = _cmark.SMART
|
|
18
|
+
GITHUB_PRE_LANG = _cmark.GITHUB_PRE_LANG
|
|
19
|
+
LIBERAL_HTML_TAG = _cmark.LIBERAL_HTML_TAG
|
|
20
|
+
FOOTNOTES = _cmark.FOOTNOTES
|
|
21
|
+
STRIKETHROUGH_DOUBLE_TILDE = _cmark.STRIKETHROUGH_DOUBLE_TILDE
|
|
22
|
+
TABLE_PREFER_STYLE_ATTRIBUTES = _cmark.TABLE_PREFER_STYLE_ATTRIBUTES
|
|
23
|
+
FULL_INFO_STRING = _cmark.FULL_INFO_STRING
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Extension(StrEnum):
|
|
27
|
+
"""cmark-gfm's core syntax extensions, for render_html's extensions."""
|
|
28
|
+
|
|
29
|
+
AUTOLINK = "autolink"
|
|
30
|
+
STRIKETHROUGH = "strikethrough"
|
|
31
|
+
TABLE = "table"
|
|
32
|
+
TAGFILTER = "tagfilter"
|
|
33
|
+
TASKLIST = "tasklist"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["Extension", "Option", "render_html"]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/* Renders markdown with cmark-gfm under caller-chosen CMARK_OPT_* flags and
|
|
2
|
+
* core syntax extensions. With neither, it is cmark-gfm's default: plain
|
|
3
|
+
* CommonMark under CMARK_OPT_DEFAULT.
|
|
4
|
+
*
|
|
5
|
+
* Every allocation cmark hands back is freed here, so repeated renders don't
|
|
6
|
+
* grow memory.
|
|
7
|
+
*/
|
|
8
|
+
#define PY_SSIZE_T_CLEAN
|
|
9
|
+
#include <Python.h>
|
|
10
|
+
|
|
11
|
+
#include <pthread.h>
|
|
12
|
+
#include <string.h>
|
|
13
|
+
|
|
14
|
+
#include "cmark-gfm-core-extensions.h"
|
|
15
|
+
#include "cmark-gfm.h"
|
|
16
|
+
|
|
17
|
+
/* Every flag that changes parsing or HTML output. CMARK_OPT_SAFE and
|
|
18
|
+
* CMARK_OPT_NORMALIZE are left out: cmark-gfm keeps them as no-ops. */
|
|
19
|
+
#define KNOWN_OPTIONS \
|
|
20
|
+
(CMARK_OPT_SOURCEPOS | CMARK_OPT_HARDBREAKS | CMARK_OPT_UNSAFE | \
|
|
21
|
+
CMARK_OPT_NOBREAKS | CMARK_OPT_VALIDATE_UTF8 | CMARK_OPT_SMART | \
|
|
22
|
+
CMARK_OPT_GITHUB_PRE_LANG | CMARK_OPT_LIBERAL_HTML_TAG | \
|
|
23
|
+
CMARK_OPT_FOOTNOTES | CMARK_OPT_STRIKETHROUGH_DOUBLE_TILDE | \
|
|
24
|
+
CMARK_OPT_TABLE_PREFER_STYLE_ATTRIBUTES | CMARK_OPT_FULL_INFO_STRING)
|
|
25
|
+
|
|
26
|
+
/* cmark-gfm's core extensions, looked up by name when the module loads. */
|
|
27
|
+
#define N_EXTENSIONS 5
|
|
28
|
+
static const char *const extension_names[N_EXTENSIONS] = {
|
|
29
|
+
"autolink", "strikethrough", "table", "tagfilter", "tasklist",
|
|
30
|
+
};
|
|
31
|
+
static cmark_syntax_extension *extensions[N_EXTENSIONS];
|
|
32
|
+
|
|
33
|
+
/* While parsing inlines, cmark adds each extension's characters ("~" for
|
|
34
|
+
* strikethrough) to a global table and clears them after
|
|
35
|
+
* (cmark_manage_extensions_special_characters), so a concurrent parse could
|
|
36
|
+
* lose its strikethrough. Renders run without the GIL, and parses one at a
|
|
37
|
+
* time. */
|
|
38
|
+
static pthread_mutex_t render_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
39
|
+
|
|
40
|
+
/* A fork while another thread holds render_lock would leave the child with a
|
|
41
|
+
* lock nothing releases. Forking waits for the render and unlocks in both. */
|
|
42
|
+
static void lock_render(void) { pthread_mutex_lock(&render_lock); }
|
|
43
|
+
static void unlock_render(void) { pthread_mutex_unlock(&render_lock); }
|
|
44
|
+
|
|
45
|
+
/* Fills chosen with the extensions named by names, in the caller's order and
|
|
46
|
+
* each once, since cmark would run a twice-attached extension twice. Returns
|
|
47
|
+
* how many, or -1 with an exception set. */
|
|
48
|
+
static int choose_extensions(PyObject *names,
|
|
49
|
+
cmark_syntax_extension *chosen[N_EXTENSIONS]) {
|
|
50
|
+
/* A str is iterable too, but "table" would mean "t", "a", ... */
|
|
51
|
+
if (PyUnicode_Check(names)) {
|
|
52
|
+
PyErr_SetString(PyExc_TypeError,
|
|
53
|
+
"extensions must be an iterable of str, not a str");
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
PyObject *iterator = PyObject_GetIter(names);
|
|
57
|
+
if (iterator == NULL) {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
int count = 0;
|
|
61
|
+
int attached[N_EXTENSIONS] = {0};
|
|
62
|
+
PyObject *name;
|
|
63
|
+
while ((name = PyIter_Next(iterator)) != NULL) {
|
|
64
|
+
int found = -1;
|
|
65
|
+
if (PyUnicode_Check(name)) {
|
|
66
|
+
for (int i = 0; i < N_EXTENSIONS; i++) {
|
|
67
|
+
if (PyUnicode_CompareWithASCIIString(name, extension_names[i]) == 0) {
|
|
68
|
+
found = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (found < 0) {
|
|
74
|
+
PyErr_Format(PyExc_ValueError, "unknown cmark-gfm extension: %R", name);
|
|
75
|
+
Py_DECREF(name);
|
|
76
|
+
Py_DECREF(iterator);
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
Py_DECREF(name);
|
|
80
|
+
if (!attached[found]) {
|
|
81
|
+
attached[found] = 1;
|
|
82
|
+
chosen[count++] = extensions[found];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
Py_DECREF(iterator);
|
|
86
|
+
return PyErr_Occurred() ? -1 : count;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static PyObject *render_html(PyObject *module, PyObject *args,
|
|
90
|
+
PyObject *kwargs) {
|
|
91
|
+
static char *keywords[] = {"", "options", "extensions", NULL};
|
|
92
|
+
PyObject *arg;
|
|
93
|
+
int options = CMARK_OPT_DEFAULT;
|
|
94
|
+
PyObject *names = NULL;
|
|
95
|
+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "U|iO:render_html", keywords,
|
|
96
|
+
&arg, &options, &names)) {
|
|
97
|
+
return NULL;
|
|
98
|
+
}
|
|
99
|
+
if (options & ~KNOWN_OPTIONS) {
|
|
100
|
+
PyErr_Format(PyExc_ValueError, "unknown cmark options: %#x",
|
|
101
|
+
(unsigned)(options & ~KNOWN_OPTIONS));
|
|
102
|
+
return NULL;
|
|
103
|
+
}
|
|
104
|
+
cmark_syntax_extension *chosen[N_EXTENSIONS];
|
|
105
|
+
int n_chosen = names == NULL ? 0 : choose_extensions(names, chosen);
|
|
106
|
+
if (n_chosen < 0) {
|
|
107
|
+
return NULL;
|
|
108
|
+
}
|
|
109
|
+
Py_ssize_t length;
|
|
110
|
+
const char *text = PyUnicode_AsUTF8AndSize(arg, &length);
|
|
111
|
+
if (text == NULL) {
|
|
112
|
+
return NULL;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* text points into the str argument, which the caller keeps alive for the
|
|
116
|
+
* call. */
|
|
117
|
+
char *html = NULL;
|
|
118
|
+
Py_BEGIN_ALLOW_THREADS
|
|
119
|
+
/* Taken without the GIL, so a thread waiting here can't block one that
|
|
120
|
+
* holds the lock and needs the GIL back. */
|
|
121
|
+
pthread_mutex_lock(&render_lock);
|
|
122
|
+
cmark_parser *parser = cmark_parser_new(options);
|
|
123
|
+
for (int i = 0; i < n_chosen; i++) {
|
|
124
|
+
cmark_parser_attach_syntax_extension(parser, chosen[i]);
|
|
125
|
+
}
|
|
126
|
+
cmark_parser_feed(parser, text, (size_t)length);
|
|
127
|
+
/* The parser hands the tree to the caller; cmark_parser_free leaves it. */
|
|
128
|
+
cmark_node *document = cmark_parser_finish(parser);
|
|
129
|
+
/* finish has cleared the special characters again, and rendering only
|
|
130
|
+
* reads the tree and the extensions, so it runs alongside other parses. */
|
|
131
|
+
pthread_mutex_unlock(&render_lock);
|
|
132
|
+
if (document != NULL) {
|
|
133
|
+
html = cmark_render_html(document, options,
|
|
134
|
+
cmark_parser_get_syntax_extensions(parser));
|
|
135
|
+
cmark_node_free(document);
|
|
136
|
+
}
|
|
137
|
+
cmark_parser_free(parser);
|
|
138
|
+
Py_END_ALLOW_THREADS
|
|
139
|
+
|
|
140
|
+
if (html == NULL) {
|
|
141
|
+
PyErr_SetString(PyExc_RuntimeError, "cmark returned no document");
|
|
142
|
+
return NULL;
|
|
143
|
+
}
|
|
144
|
+
PyObject *result = PyUnicode_DecodeUTF8(html, (Py_ssize_t)strlen(html), NULL);
|
|
145
|
+
/* cmark_render_html allocates with the tree's allocator, which is the
|
|
146
|
+
* default one for a parser from cmark_parser_new. */
|
|
147
|
+
cmark_get_default_mem_allocator()->free(html);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static int add_options(PyObject *module) {
|
|
152
|
+
#define ADD(name) \
|
|
153
|
+
if (PyModule_AddIntConstant(module, #name, CMARK_OPT_##name) < 0) { \
|
|
154
|
+
return -1; \
|
|
155
|
+
}
|
|
156
|
+
ADD(SOURCEPOS)
|
|
157
|
+
ADD(HARDBREAKS)
|
|
158
|
+
ADD(UNSAFE)
|
|
159
|
+
ADD(NOBREAKS)
|
|
160
|
+
ADD(VALIDATE_UTF8)
|
|
161
|
+
ADD(SMART)
|
|
162
|
+
ADD(GITHUB_PRE_LANG)
|
|
163
|
+
ADD(LIBERAL_HTML_TAG)
|
|
164
|
+
ADD(FOOTNOTES)
|
|
165
|
+
ADD(STRIKETHROUGH_DOUBLE_TILDE)
|
|
166
|
+
ADD(TABLE_PREFER_STYLE_ATTRIBUTES)
|
|
167
|
+
ADD(FULL_INFO_STRING)
|
|
168
|
+
#undef ADD
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static int exec_module(PyObject *module) {
|
|
173
|
+
static int fork_handlers_registered = 0;
|
|
174
|
+
if (!fork_handlers_registered) {
|
|
175
|
+
if (pthread_atfork(lock_render, unlock_render, unlock_render) != 0) {
|
|
176
|
+
PyErr_SetString(PyExc_ImportError, "pthread_atfork failed");
|
|
177
|
+
return -1;
|
|
178
|
+
}
|
|
179
|
+
fork_handlers_registered = 1;
|
|
180
|
+
}
|
|
181
|
+
cmark_gfm_core_extensions_ensure_registered();
|
|
182
|
+
for (int i = 0; i < N_EXTENSIONS; i++) {
|
|
183
|
+
extensions[i] = cmark_find_syntax_extension(extension_names[i]);
|
|
184
|
+
if (extensions[i] == NULL) {
|
|
185
|
+
PyErr_Format(PyExc_ImportError, "cmark-gfm is missing the %s extension",
|
|
186
|
+
extension_names[i]);
|
|
187
|
+
return -1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return add_options(module);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
PyDoc_STRVAR(render_html_doc,
|
|
194
|
+
"render_html($module, text, /, options=0, extensions=())\n--\n\n"
|
|
195
|
+
"Render markdown to HTML. options is a bitwise OR of cmark-gfm's "
|
|
196
|
+
"CMARK_OPT_* flags; extensions names core extensions to attach: "
|
|
197
|
+
"autolink, strikethrough, table, tagfilter, tasklist.");
|
|
198
|
+
|
|
199
|
+
static PyMethodDef methods[] = {
|
|
200
|
+
{"render_html", (PyCFunction)(void (*)(void))render_html,
|
|
201
|
+
METH_VARARGS | METH_KEYWORDS, render_html_doc},
|
|
202
|
+
{NULL, NULL, 0, NULL},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
static PyModuleDef_Slot slots[] = {
|
|
206
|
+
{Py_mod_exec, exec_module},
|
|
207
|
+
{0, NULL},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
static struct PyModuleDef module_def = {
|
|
211
|
+
PyModuleDef_HEAD_INIT,
|
|
212
|
+
.m_name = "cmark_render._cmark",
|
|
213
|
+
.m_doc = "Markdown rendering with cmark-gfm.",
|
|
214
|
+
.m_size = 0,
|
|
215
|
+
.m_methods = methods,
|
|
216
|
+
.m_slots = slots,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
PyMODINIT_FUNC PyInit__cmark(void) { return PyModuleDef_Init(&module_def); }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
SOURCEPOS: int
|
|
4
|
+
HARDBREAKS: int
|
|
5
|
+
UNSAFE: int
|
|
6
|
+
NOBREAKS: int
|
|
7
|
+
VALIDATE_UTF8: int
|
|
8
|
+
SMART: int
|
|
9
|
+
GITHUB_PRE_LANG: int
|
|
10
|
+
LIBERAL_HTML_TAG: int
|
|
11
|
+
FOOTNOTES: int
|
|
12
|
+
STRIKETHROUGH_DOUBLE_TILDE: int
|
|
13
|
+
TABLE_PREFER_STYLE_ATTRIBUTES: int
|
|
14
|
+
FULL_INFO_STRING: int
|
|
15
|
+
|
|
16
|
+
def render_html(
|
|
17
|
+
text: str, /, options: int = ..., extensions: Iterable[str] = ...
|
|
18
|
+
) -> str: ...
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmark-render
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Fast, leak-free markdown to HTML with a vendored cmark-gfm (tables, ~~strikethrough~~, raw HTML).
|
|
5
|
+
Author: Zachary Juang
|
|
6
|
+
License-Expression: BSD-2-Clause
|
|
7
|
+
Project-URL: Source, https://github.com/zachary822/cmark-render
|
|
8
|
+
Keywords: markdown,commonmark,cmark,gfm
|
|
9
|
+
Classifier: Programming Language :: C
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
12
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
License-File: vendor/cmark-gfm/COPYING
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# cmark-render
|
|
21
|
+
|
|
22
|
+
Markdown to HTML in C, using a vendored, statically compiled
|
|
23
|
+
[cmark-gfm](https://github.com/github/cmark-gfm).
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from cmark_render import Extension, Option, render_html
|
|
27
|
+
|
|
28
|
+
render_html(
|
|
29
|
+
"| a | b |\n|---|---|\n| ~~old~~ | **new** |",
|
|
30
|
+
extensions=[Extension.TABLE, Extension.STRIKETHROUGH],
|
|
31
|
+
)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
With no options or extensions, `render_html` is plain cmark-gfm: CommonMark
|
|
35
|
+
with its default options, so raw HTML and `javascript:`-style links are
|
|
36
|
+
dropped.
|
|
37
|
+
|
|
38
|
+
## Extensions
|
|
39
|
+
|
|
40
|
+
`extensions` takes the names of cmark-gfm's core syntax extensions, as
|
|
41
|
+
`Extension` members or plain strings. None are attached by default.
|
|
42
|
+
|
|
43
|
+
| `Extension` | Effect |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `TABLE` | GFM pipe tables. |
|
|
46
|
+
| `STRIKETHROUGH` | `~~text~~` and `~text~` become `<del>`. |
|
|
47
|
+
| `AUTOLINK` | Bare `www.` and `https://` URLs become links. |
|
|
48
|
+
| `TASKLIST` | `- [ ]` and `- [x]` list items become checkboxes. |
|
|
49
|
+
| `TAGFILTER` | Escapes `<script>`, `<iframe>`, `<style>` and other risky tags even under `UNSAFE`. |
|
|
50
|
+
|
|
51
|
+
An unknown name raises `ValueError`, and repeating a name has no extra effect.
|
|
52
|
+
|
|
53
|
+
## Options
|
|
54
|
+
|
|
55
|
+
`options` takes cmark-gfm's `CMARK_OPT_*` flags, as `Option` members combined
|
|
56
|
+
with `|`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
render_html(text, options=Option.SMART | Option.FOOTNOTES)
|
|
60
|
+
render_html(text, options=Option.UNSAFE) # keep raw HTML; sanitize untrusted input
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| `Option` | Effect |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `UNSAFE` | Pass raw HTML and `javascript:`-style links through. **Sanitize the output** if the input is untrusted. |
|
|
66
|
+
| `STRIKETHROUGH_DOUBLE_TILDE` | With `STRIKETHROUGH`, strike through only on `~~`, leaving single `~` alone. |
|
|
67
|
+
| `SMART` | Curly quotes, en and em dashes, ellipses. |
|
|
68
|
+
| `HARDBREAKS` | Render soft line breaks as `<br />`. |
|
|
69
|
+
| `NOBREAKS` | Render soft line breaks as spaces. |
|
|
70
|
+
| `SOURCEPOS` | Add `data-sourcepos` attributes to block elements. |
|
|
71
|
+
| `FOOTNOTES` | Parse `[^1]` footnotes. |
|
|
72
|
+
| `VALIDATE_UTF8` | Accepted, but has no effect: a `str` always arrives as valid UTF-8. |
|
|
73
|
+
| `GITHUB_PRE_LANG` | Write `<pre lang="x">` for fenced code instead of a `class` on `<code>`. |
|
|
74
|
+
| `FULL_INFO_STRING` | Keep the rest of a code fence's info string, in `data-meta`. |
|
|
75
|
+
| `LIBERAL_HTML_TAG` | Be looser about what counts as an inline HTML tag. |
|
|
76
|
+
| `TABLE_PREFER_STYLE_ATTRIBUTES` | With `TABLE`, align cells with `style` instead of `align`. |
|
|
77
|
+
|
|
78
|
+
An unknown bit raises `ValueError`.
|
|
79
|
+
|
|
80
|
+
## Memory and threads
|
|
81
|
+
|
|
82
|
+
Every render frees its syntax tree and rendered HTML, so memory stays flat
|
|
83
|
+
however many documents you render.
|
|
84
|
+
|
|
85
|
+
It is safe to use with threads and `fork()`. Renders run without the GIL.
|
|
86
|
+
Parsing runs one at a time, because cmark-gfm keeps a global table of special
|
|
87
|
+
characters that changes during each parse, but writing the HTML runs in
|
|
88
|
+
parallel. A `pthread_atfork` handler stops a
|
|
89
|
+
child process from inheriting a held lock.
|
|
90
|
+
|
|
91
|
+
## Install
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
pip install cmark-render
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Wheels are built for CPython 3.14 on macOS arm64 and manylinux x86_64 and
|
|
98
|
+
aarch64. On other platforms, pip builds from the sdist, which needs a C
|
|
99
|
+
compiler.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
BSD-2-Clause. The vendored cmark-gfm is under its own licenses, in
|
|
104
|
+
`vendor/cmark-gfm/COPYING`. `vendor/cmark-gfm/UPSTREAM` records the upstream
|
|
105
|
+
commit.
|