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.
Files changed (91) hide show
  1. cmark_render-0.2.1/PKG-INFO +105 -0
  2. cmark_render-0.2.1/README.md +86 -0
  3. {cmark_render-0.1.0 → cmark_render-0.2.1}/pyproject.toml +7 -1
  4. cmark_render-0.2.1/src/cmark_render/__init__.py +36 -0
  5. cmark_render-0.2.1/src/cmark_render/_cmark.c +219 -0
  6. cmark_render-0.2.1/src/cmark_render/_cmark.pyi +18 -0
  7. cmark_render-0.2.1/src/cmark_render.egg-info/PKG-INFO +105 -0
  8. cmark_render-0.2.1/tests/test_render.py +197 -0
  9. cmark_render-0.1.0/PKG-INFO +0 -65
  10. cmark_render-0.1.0/README.md +0 -46
  11. cmark_render-0.1.0/src/cmark_render/__init__.py +0 -5
  12. cmark_render-0.1.0/src/cmark_render/_cmark.c +0 -122
  13. cmark_render-0.1.0/src/cmark_render/_cmark.pyi +0 -1
  14. cmark_render-0.1.0/src/cmark_render.egg-info/PKG-INFO +0 -65
  15. cmark_render-0.1.0/tests/test_render.py +0 -108
  16. {cmark_render-0.1.0 → cmark_render-0.2.1}/LICENSE +0 -0
  17. {cmark_render-0.1.0 → cmark_render-0.2.1}/MANIFEST.in +0 -0
  18. {cmark_render-0.1.0 → cmark_render-0.2.1}/setup.cfg +0 -0
  19. {cmark_render-0.1.0 → cmark_render-0.2.1}/setup.py +0 -0
  20. {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render/py.typed +0 -0
  21. {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/SOURCES.txt +0 -0
  22. {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/dependency_links.txt +0 -0
  23. {cmark_render-0.1.0 → cmark_render-0.2.1}/src/cmark_render.egg-info/top_level.txt +0 -0
  24. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/COPYING +0 -0
  25. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/UPSTREAM +0 -0
  26. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/autolink.c +0 -0
  27. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/autolink.h +0 -0
  28. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/cmark-gfm-core-extensions.h +0 -0
  29. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/core-extensions.c +0 -0
  30. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/ext_scanners.c +0 -0
  31. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/ext_scanners.h +0 -0
  32. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/strikethrough.c +0 -0
  33. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/strikethrough.h +0 -0
  34. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/table.c +0 -0
  35. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/table.h +0 -0
  36. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tagfilter.c +0 -0
  37. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tagfilter.h +0 -0
  38. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tasklist.c +0 -0
  39. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/extensions/tasklist.h +0 -0
  40. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/cmark-gfm_export.h +0 -0
  41. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/cmark-gfm_version.h +0 -0
  42. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/generated/config.h +0 -0
  43. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/arena.c +0 -0
  44. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/blocks.c +0 -0
  45. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/buffer.c +0 -0
  46. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/buffer.h +0 -0
  47. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/case_fold_switch.inc +0 -0
  48. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/chunk.h +0 -0
  49. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark-gfm-extension_api.h +0 -0
  50. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark-gfm.h +0 -0
  51. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark.c +0 -0
  52. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark_ctype.c +0 -0
  53. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/cmark_ctype.h +0 -0
  54. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/commonmark.c +0 -0
  55. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/entities.inc +0 -0
  56. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/footnotes.c +0 -0
  57. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/footnotes.h +0 -0
  58. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini.h +0 -0
  59. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_href_e.c +0 -0
  60. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_html_e.c +0 -0
  61. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/houdini_html_u.c +0 -0
  62. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/html.c +0 -0
  63. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/html.h +0 -0
  64. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/inlines.c +0 -0
  65. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/inlines.h +0 -0
  66. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/iterator.c +0 -0
  67. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/iterator.h +0 -0
  68. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/latex.c +0 -0
  69. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/linked_list.c +0 -0
  70. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/man.c +0 -0
  71. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/map.c +0 -0
  72. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/map.h +0 -0
  73. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/node.c +0 -0
  74. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/node.h +0 -0
  75. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/parser.h +0 -0
  76. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plaintext.c +0 -0
  77. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plugin.c +0 -0
  78. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/plugin.h +0 -0
  79. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/references.c +0 -0
  80. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/references.h +0 -0
  81. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/registry.c +0 -0
  82. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/registry.h +0 -0
  83. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/render.c +0 -0
  84. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/render.h +0 -0
  85. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/scanners.c +0 -0
  86. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/scanners.h +0 -0
  87. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/syntax_extension.c +0 -0
  88. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/syntax_extension.h +0 -0
  89. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/utf8.c +0 -0
  90. {cmark_render-0.1.0 → cmark_render-0.2.1}/vendor/cmark-gfm/src/utf8.h +0 -0
  91. {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.0"
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.