carve (Ruby)
Native Ruby bindings for the Carve
markup language. This gem is a thin native extension built with
magnus + rb-sys
over the carve-rs engine. The parser
is not reimplemented in Ruby; it calls into the Rust crate directly, mirroring
how Djot's djotter gem wraps the jotdown crate.
Install
# Gemfile
gem "carve-lang"
bundle install
Or install directly:
gem install carve-lang
Then require "carve" as normal - the gem distribution name is carve-lang
but the require path stays carve.
Building from source requires a Rust toolchain (cargo, Rust >= 1.75) and
Ruby development headers. RubyGems compiles the native extension at install
time via rb_sys.
Usage
require "carve"
Carve.to_html("# Hello *world*")
# => "<section id=\"Hello-world\">\n <h1>Hello <strong>world</strong></h1>\n</section>"
# Carve syntax note: *...* is STRONG (bold), /.../ is EMPHASIS (italic).
Carve.to_html("*bold* and /italic/")
# Enable opt-in extensions (Symbols or Strings, snake_case or hyphenated):
Carve.to_html(<<~CRV, extensions: [:math_block])
```math
a^2 + b^2 = c^2
CRV
Carve.to_html(src, extensions: %w[math-block list-table])
### Recognized extensions
`autolink`, `details`, `list_table`, `math_block`, `heading_permalinks`,
`citations`, `code_callouts`, `tab_normalize`, `wikilinks`, `external_links`,
`fenced_render`, `fenced_render_graphviz`, `fenced_render_chart`, `spoiler`,
`table_of_contents` (see `Carve::EXTENSIONS`).
An unknown extension name raises `ArgumentError`.
## Parsing to an AST
`Carve.parse` returns the parsed document as a tree of Ruby Hashes and Arrays,
for consumers that want to walk or transform the document rather than render
HTML - for example a custom PDF renderer (see
[`carve-hexapdf`](https://github.com/markup-carve/carve-hexapdf)).
```ruby
Carve.parse("# Hello *world*")
# => {type: "document", frontmatter: {}, footnote_defs: {},
# children: [{type: "heading", level: 1,
# children: [{type: "text", value: "Hello "},
# {type: "emphasis", kind: "strong",
# children: [{type: "text", value: "world"}], attrs: nil}],
# attrs: nil}],
# source_len: 15}
Every node is a Hash with a :type key plus its fields; child collections are
Arrays; :attrs is nil or a Hash of {id:, classes:, key_values:}. Keys are
symbols. This is the raw parse tree (default profile, no extensions), so
render-stage extension rewrites are not applied.
Static render mode + renderers
By default Carve.to_html renders interactive HTML: client-script
constructs (Mermaid/Graphviz/Chart diagrams, math) emit hydration elements
(<pre class="mermaid">, ...) and disclosure stays collapsed (<details>).
Pass mode: :static to emit self-contained HTML for print, PDF, or
archival. Static mode forces disclosure (<details open>) and pre-renders
client-script constructs through the renderers: callables you supply.
Carve.to_html(<<~CRV, extensions: [:fenced_render], mode: :static,
renderers: { mermaid: ->(src) { "<svg>#{src}</svg>" } })
```mermaid
graph TD; A-->B
CRV
### Renderer callable signatures
The `renderers:` Hash is keyed by Symbol or String (see
`Carve::RENDERER_KEYS`):
| Key | Callable signature | Receives |
| --- | ------------------ | -------- |
| `:mermaid` | `(String) -> String` | the diagram source |
| `:chart` | `(String) -> String` | the chart JSON source |
| `:graphviz` | `(String) -> String` | the DOT / Graphviz source |
| `:math` | `(String, display) -> String` | the TeX source and a `display` boolean (`true` for block / display math, `false` for inline) |
Each callable returns a self-contained HTML string (an `<svg>` / `<img>` for a
diagram, MathML / HTML for math) that the engine emits **verbatim** on the
static path.
### Source fallback (graceful degradation)
When the renderer a construct needs is **absent**, or a supplied renderer
**raises** or returns a **non-String**, the construct degrades to its source -
never blank, and never raw HTML. The fallback source is **HTML-escaped**, so a
construct body containing markup (e.g. `<img onerror=...>`) can never inject raw
HTML. This is part of the cross-implementation graceful-degradation rollout
(spec carve #205; siblings carve-js #242, carve-php #240, carve-rs #143,
carve-py #1).
An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
## API
| Method | Description |
| ------ | ----------- |
| `Carve.to_html(source)` | Render Carve source to HTML. |
| `Carve.parse(source)` | Parse Carve source into an AST (tree of Ruby Hashes/Arrays). |
| `Carve.to_html(source, extensions: [...])` | Render with the named extensions enabled. |
| `Carve.to_html(source, mode: :static, renderers: {...})` | Render self-contained static HTML with build-time renderers. |
| `Carve.to_html_with_extensions(source, names_array)` | Native primitive (Array of Strings). |
| `Carve.to_html_full(source, names_array, mode_string, renderers_hash)` | Native static-mode primitive. |
| `Carve::VERSION` | Gem version. |
| `Carve::EXTENSIONS` | Array of recognized extension symbols. |
| `Carve::MODES` | Array of recognized render modes (`:interactive`, `:static`). |
| `Carve::RENDERER_KEYS` | Array of recognized `renderers:` keys. |
## Develop
```sh
bundle install
rake compile # builds the Rust extension into lib/carve/carve.so
rake test # runs the minitest suite
[!NOTE] The native build uses
rb_sys+bindgen(libclang) to read Ruby's headers. On systems where libclang cannot find its builtin C headers (the'stdarg.h' file not founderror), point it at the GCC builtin include dir:export BINDGEN_EXTRA_CLANG_ARGS="-I/usr/lib/gcc/x86_64-linux-gnu/13/include"(Adjust the GCC version directory to match your toolchain.)
carve-rs dependency pin
ext/carve/Cargo.toml pins a specific carve-rs commit for reproducible gem
builds:
carve_rs = { package = "carve", git = "https://github.com/markup-carve/carve-rs", rev = "09c64118223c1574d136c308cfe2a70df8ab5128" }
The crate is imported under the alias carve_rs. Note: carve-rs has since
renamed its published crate to carve-lang on main; this pin predates that
rename, so package = "carve" is correct for this exact rev. When bumping to
a newer carve-rs commit (past the rename), update both the rev and
package = "carve-lang".
License
MIT, markup-carve.