carve (Python binding)
Native Python bindings for the Carve markup language. This package is a thin PyO3 binding over the Rust implementation carve-rs, so the parser is not reimplemented in Python: every conversion delegates to the same engine the Rust CLI and WASM builds use. Output is byte-identical to carve-rs for the same input.
This unlocks the Python docs / data ecosystem (MkDocs, Sphinx, Pelican, Jupyter/nbconvert) for Carve.
Install
Wheels are abi3 (abi3-py38), so a single wheel covers CPython 3.8+.
From a built wheel:
pip install carve-lang
From source (needs a Rust toolchain, 1.75+):
pip install maturin
maturin develop --release # build + install into the active venv
# or
maturin build --release # produce a wheel under target/wheels/
Usage
import carve
print(carve.__version__)
# Core conversion (no extensions)
html = carve.to_html("# Hello *world*")
# -> '<section id="Hello-world">\n <h1>Hello <strong>world</strong></h1>\n</section>'
# Inline emphasis: /italic/ and *bold*
carve.to_html("/italic/ and *bold*")
# Enable opt-in extensions by name
html = carve.to_html(source, extensions=["math_block", "list_table"])
# Dedicated explicit-list variant
html = carve.to_html_with_extensions(source, ["autolink"])
# Map `:name:` symbols to their values
carve.to_html("Ship it :rocket:", symbols={"rocket": "🚀"})
# -> '<p>Ship it 🚀</p>'
# Other renderers
carve.to_markdown(source)
carve.to_plain_text(source)
carve.to_ansi(source)
# Discover supported extension names
carve.extensions()
Passing an unknown extension name raises ValueError.
The names come from the engine itself, so carve.extensions() is the list this
build actually accepts rather than a list documented here that could fall behind
it. They are kebab-case (math-block, table-of-contents); the snake_case
spellings this binding has always taken (math_block) reach the same
extensions, so nothing written against the older names has to change.
The parsed AST
carve.parse() returns the document as Python data - the PART 12 exchange
shape, the same tree every Carve
engine publishes, so a consumer written against one implementation reads
another's output.
ast = carve.parse("# Title\n\nBody[^a].\n\n[^a]: note\n")
ast["type"] # "document"
[c["type"] for c in ast["children"]] # ["heading", "paragraph", "footnote"]
ast["children"][0]["pos"] # {"startLine": 1, "startColumn": 1, ...}
carve.parse_json(source) # the same tree, as a JSON string
The root carries exactly type, children and srcByteLength; frontmatter and
footnote definitions are block nodes inside children, not root fields. Every
node except the root carries pos when the engine could place it - 1-based
lines and columns, 0-based offsets, ends exclusive, counted in Unicode
codepoints, not bytes. A node the engine could not place, such as
reassembled table-cell text, carries no pos at all rather than an invented
one.
The serialization is the engine's own, so this binding publishes byte-identical
output to the carve --json CLI and to every other binding over carve-rs.
Symbols
A :name: symbol renders its literal :name: source unless the name is in the
symbols map passed as the symbols= keyword (supported by to_html and
to_html_with_extensions):
carve.to_html("Ship it :rocket: :shrug:", symbols={"rocket": "🚀"})
# -> '<p>Ship it 🚀 :shrug:</p>' (an unmapped name stays literal)
The leading word-boundary guard is unaffected by an active map: a:b:c,
10:30: and me@example.com never become symbols.
Security: symbol values are TRUSTED RAW output. A mapped value is inserted into the output unescaped - the same trust class as a
rendererscallable.symbols={"b": "<b>x</b>"}emits a real<b>element, not escaped text. This is deliberate (processor configuration is trusted). Never build a symbols map out of untrusted / user-supplied input.
Untrusted input
Carve's normative hardening is always on and needs no argument: dangerous URL
schemes are blanked, event-handler attributes like onclick are dropped, and the
bidi override/isolate characters behind Trojan Source are removed from rendered
text.
Raw passthrough is the deliberate exception. A ```=html block or a
`…`{=html} span renders verbatim by design, so it is the one thing
input you did not author has to switch off:
html = carve.to_html(user_input, safe=True, profile="comment")
safe=True escapes those raw blocks and spans instead of emitting them. It is
HTML-only, because HTML is the only target that can emit live markup:
to_markdown escapes raw HTML, to_plain_text drops it, to_ansi keeps it as
terminal text.
profile restricts which constructs are allowed at all and caps input length -
"full", "article", "comment", "minimal", or None. It applies to every
target, including to_markdown / to_plain_text / to_ansi.
An unknown profile name raises ValueError, and so does a rejection - input
past the profile's max length, or a denied construct when the action is error:
carve.to_html("x" * 20_000, profile="minimal")
# ValueError: Profile violations: 'document' is not allowed: max_length_exceeded (...)
It raises rather than returning something that looks like output: the engine's infallible entry point answers a rejection with an empty string, which a caller cannot tell from a document that legitimately rendered to nothing.
Full recipe, defaults and threat model: Security.
Stored documents and spec versions
carve fmt --stamp (in any Carve engine) records the spec version a document was
last processed under. This binding reads that marker back, so a repository of
stored .crv files can be checked for documents predating a breaking spec
change:
carve.read_stamp(source)
# {'version': '0.1', 'generated_by': 'carve-php 0.1.0'}
carve.needs_review(source) # True when the document predates this engine
An unstamped document answers True: its provenance is unknown, and assuming
it is current is the unsafe direction. Both marker forms are read, and a marker
written by any engine reads the same - the format is the contract, not any one
API.
What a version difference means is the
versioning contract: only
[behavior] changelog entries between the stamped version and yours can require
a document change.
Supported extensions
The string passed in extensions=[...] maps to a carve-rs extension:
| name | effect |
|---|---|
autolink |
turn bare URLs into links |
details |
collapsible <details> blocks |
external_links |
mark external links (rel/target) |
fenced_render |
render fenced blocks of a target language (mermaid) |
fenced_render_chart |
render fenced chart blocks (Chart.js) |
fenced_render_plantuml |
render plantuml/puml blocks (Kroki client) |
fenced_render_graphviz |
render dot/graphviz blocks |
fenced_render_d2 |
render d2 blocks |
fenced_render_wavedrom |
render wavedrom blocks |
fenced_render_vega_lite |
render vega-lite blocks (Vega-Lite) |
fenced_render_abc |
render abc music-notation blocks |
heading_permalinks |
add permalink anchors to headings |
list_table |
build tables from nested lists |
math_block |
fenced math blocks |
spoiler |
spoiler / hidden-content inline |
tab_normalize |
normalize tab indentation |
wikilinks |
[[wiki style]] links |
citations |
citation references |
code-callouts |
numbered callouts in fenced code blocks |
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file carve_lang-0.1.1.tar.gz.
File metadata
- Download URL: carve_lang-0.1.1.tar.gz
- Upload date:
- Size: 24.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
25ade7578ab1b6e2e63325e1da74f45366e4f9f22759daf5dc6dc82f753c13d6
|
|
| MD5 |
0f253aaf1043e643d6210095cca08e9c
|
|
| BLAKE2b-256 |
339e45e7e194c0f78ee3908540c03c76d316a67271ca930fae2ef485d5bac364
|
File details
Details for the file carve_lang-0.1.1-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: carve_lang-0.1.1-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c09cd3525bc5ad1b7b95584ad0c12d564c82d68105a8de130d88753ed94a5c62
|
|
| MD5 |
b048b989b314584f7b4942b80f29474a
|
|
| BLAKE2b-256 |
b9aa3951844cf1f2e447e712105791943bfe12da87b9ddd6389bc6a6e979b988
|
File details
Details for the file carve_lang-0.1.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: carve_lang-0.1.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: CPython 3.8+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c51f8bb263bd4b624ee1a59a6684764f6cc07d6ca72705ffc6e30983a0ac606
|
|
| MD5 |
2c22f755bf9ca1a3e358cb134b5e5958
|
|
| BLAKE2b-256 |
01b94c732a669e4e3c1081e32a4c000a27e3861183ee2d25b2461ef51478fd9b
|
File details
Details for the file carve_lang-0.1.1-cp38-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: carve_lang-0.1.1-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.1 MB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4407d8b4f90a072b8fd1206c92e591850e46a520a702c0f544af1c1b3eab404
|
|
| MD5 |
e8991646b0b664eac445aa803bb92628
|
|
| BLAKE2b-256 |
f2dc243339b0d651e815e656557a3e855da0cb2d65df7a94817211e9b8be1c9b
|