Skip to main content

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"])

# Configure enabled extensions (the mapping is keyword-only)
html = carve.to_html(
    source,
    extensions=["heading-permalinks", "tabs"],
    extension_options={
        "heading-permalinks": {"aria_label": "Back to text", "lowercase_ids": True},
        "tabs": {"mode": "aria"},
    },
)

# 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>'

# Engine-level options are keyword-only. Omitting one preserves its engine default.
carve.to_html("# Mixed Case", lowercase_heading_ids=True)
carve.to_html("Hello @ada", mention_url="https://example.com/{}")

# Other renderers. They take the same engine options and `extension_options`.
carve.to_markdown(source)
carve.to_plain_text(source)
carve.to_ansi(source, lowercase_heading_ids=True)
carve.to_carve(source)

# Migration returns canonical Carve plus a machine-readable loss report.
carve.from_html('<p>Hello <strong>world</strong></p>')
carve.from_markdown('*em* and **strong**')

# Discover supported extension names
carve.extensions()

Passing an unknown extension name raises ValueError. extension_options only accepts enabled extension names and their documented option keys; unknown names, unknown keys, invalid enum values and options for an extension that takes none are rejected instead of silently using defaults.

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.

Linting

carve.lint reports constructs that parse and render, but not the way the author meant. The defect class it catches is the silent one: the document parses, the renderer emits something, and what the author wrote never reaches the page.

import carve

for warning in carve.lint("{#orphan .cls}\n\n"):
    print(warning["line"], warning["rule"], warning["message"])
1 unattached-block-attribute This block attribute reaches no block: ...

That one is the clearest case - {#id .cls} above a blank line attaches to nothing, so the id and the class vanish and nothing anywhere says so.

Each warning is a dict:

Key What
line, column 1-based, for reporting
rule a stable id, shared with carve-js and carve-php - the same trigger reports the same id in every engine
message what degrades, in prose
start, end codepoint offsets into the source, 0-based, end exclusive

start and end slice the offending text directly:

source = "{#orphan .cls}\n\n"
warning = carve.lint(source)[0]
assert source[warning["start"]:warning["end"]] == "{#orphan .cls}"

They are CODEPOINT offsets, not the byte offsets the Rust API reports. That conversion is deliberate and tested: a Rust caller slices &str with bytes, but Python slices by codepoint, so passing them through unconverted mis-slices every document carrying one non-ASCII character before a warning. The unit follows the host language - carve-js converts the same positions to UTF-16 for the same reason.

extensions is the only option accepted, because it is the only one the engine's linter reads:

carve.lint(source, extensions=["details"])

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 JSON bytes

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.

Every rendering entry point (to_html, to_html_with_extensions, to_markdown, to_plain_text, to_ansi, parse and parse_json) also accepts keyword-only lowercase_heading_ids, positions, sections, source_lines, mention_url, tag_url, and profile_base_host. Boolean options accept True or False; URL/host options accept strings. When omitted, each option retains the engine's own default.

The keyword set is the same on every target so a host does not have to remember which target takes which. Most of these options describe HTML markup, so on to_markdown / to_plain_text / to_ansi they have nothing to change; lowercase_heading_ids does change those three, because all four renderers resolve </#id> crossrefs through the same heading index.

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 renderers callable. 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

Metadata

Release files for carve-lang 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for carve-lang 0.1.2
File Size Uploaded
carve_lang-0.1.2.tar.gz 34.6 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for carve-lang 0.1.2
File
carve_lang-0.1.2-cp38-abi3-win_amd64.whl CPython 3.8 abi3 Windows x86-64 Details
carve_lang-0.1.2-cp38-abi3-musllinux_1_2_x86_64.whl CPython 3.8 abi3 Linux musl 1.2+ x86-64 Details
carve_lang-0.1.2-cp38-abi3-musllinux_1_2_aarch64.whl CPython 3.8 abi3 Linux musl 1.2+ ARM64 Details
carve_lang-0.1.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.8 abi3 Linux glibc 2.17+ x86-64 Details
carve_lang-0.1.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.8 abi3 Linux glibc 2.17+ ARM64 Details
carve_lang-0.1.2-cp38-abi3-macosx_11_0_arm64.whl CPython 3.8 abi3 macOS 11.0+ ARM64 Details
carve_lang-0.1.2-cp38-abi3-macosx_10_12_x86_64.whl CPython 3.8 abi3 macOS 10.12+ x86-64 Details

Total release size: 15.3 MB

Release files / carve_lang-0.1.2.tar.gz

Download URL carve_lang-0.1.2.tar.gz
Size 34.6 kB
Tags Source
SHA-256 checksum
How to use checksums
23d7bea017e4f76ead9ff25897fc0746dff3c778c11f2f53422a8c99a0430fb3
BLAKE2b-256 checksum
How to use checksums
991958fcd42e6393bcc1916a7febca33c1c643fd91bb929166a38f9f6e654487
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-win_amd64.whl

Download URL carve_lang-0.1.2-cp38-abi3-win_amd64.whl
Size 2.0 MB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
0a8c8c61a4645e99c5fb30983136f6ef0cf35555642207958c1d3a26c0c20b01
BLAKE2b-256 checksum
How to use checksums
b6382a39425cb2428a1596841c178edd45de0f4f1f690641f63178bb6134acc1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-musllinux_1_2_x86_64.whl

Download URL carve_lang-0.1.2-cp38-abi3-musllinux_1_2_x86_64.whl
Size 2.4 MB
Tags CPython 3.8 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
31358d42ff424a5ce0eb7e9bb3d603a389162a852d7363e66e3ce3dc5a2e2c75
BLAKE2b-256 checksum
How to use checksums
6aee0f5d0631e9f951b81ed21920f58310d6a0c36a71cb8e855fbc4be7836a82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-musllinux_1_2_aarch64.whl

Download URL carve_lang-0.1.2-cp38-abi3-musllinux_1_2_aarch64.whl
Size 2.4 MB
Tags CPython 3.8 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
6e168d54b95db08e291df5e2a472a67617bd69a94f408aeb2ea8c544f40e6d78
BLAKE2b-256 checksum
How to use checksums
d0de1ced2848222229e3b22fe6765697ab5b22f2ac4ec21802b6c19fe33bc33a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL carve_lang-0.1.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 2.2 MB
Tags CPython 3.8 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
6466dea516ccc0ec16a5d063b9d83ba4fef70966bdb708b23525cca5cc9a0d85
BLAKE2b-256 checksum
How to use checksums
5e43e9853736e3baa02920e6913a5061766cb27fa25850ec63fede2d7e8ef07e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL carve_lang-0.1.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.2 MB
Tags CPython 3.8 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
239265036633cc8fa330048e5a0b990f6fd6d8fe37b4362aee197e870e458be5
BLAKE2b-256 checksum
How to use checksums
5ba1fd66b07f47025b8df467c29afc8b169c4869093b425d7acaafe538e0cceb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-macosx_11_0_arm64.whl

Download URL carve_lang-0.1.2-cp38-abi3-macosx_11_0_arm64.whl
Size 2.0 MB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9e5d23b7b70f6026a5d18649458ee00c5f76830b78c18bdf4ec2a3017391d4a8
BLAKE2b-256 checksum
How to use checksums
6647844bb68e4271b28952a64f4fba9baaabf8ff3ff898eb618742c1f6abb8a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / carve_lang-0.1.2-cp38-abi3-macosx_10_12_x86_64.whl

Download URL carve_lang-0.1.2-cp38-abi3-macosx_10_12_x86_64.whl
Size 2.1 MB
Tags CPython 3.8 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
231eaebb1d9339b639d65b6bbae9a256dedae69a90701c7faa2f048983cbbcbe
BLAKE2b-256 checksum
How to use checksums
fd53f079e64a717fe862fcc90ecfce5957732af90fcdf47505c6a48f206b4caa
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

0.1.8

8 release files

0.1.7

8 release files

0.1.6

8 release files

0.1.5

8 release files

0.1.4

8 release files

0.1.3

8 release files

This release

0.1.2 This release

8 release files

0.1.1

4 release files

0.1.0

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page