carve-lang 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +185 -0
- data/ext/carve/Cargo.lock +343 -0
- data/ext/carve/Cargo.toml +28 -0
- data/ext/carve/extconf.rb +9 -0
- data/ext/carve/src/ast_json.rs +508 -0
- data/ext/carve/src/lib.rs +288 -0
- data/lib/carve/version.rb +5 -0
- data/lib/carve.rb +120 -0
- metadata +112 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
//! Native Ruby binding for the Carve markup language.
|
|
2
|
+
//!
|
|
3
|
+
//! Wraps the `carve` crate (carve-rs) with magnus, exposing a `Carve` Ruby
|
|
4
|
+
//! module:
|
|
5
|
+
//!
|
|
6
|
+
//! * `Carve.to_html(source)` -> HTML String
|
|
7
|
+
//! * `Carve.to_html_with_extensions(source, extensions)` -> HTML String,
|
|
8
|
+
//! where `extensions` is an Array of extension name Strings/Symbols.
|
|
9
|
+
//! * `Carve.to_html_full(source, extensions, mode, renderers)` -> HTML String,
|
|
10
|
+
//! the static-render-mode primitive: `mode` is `"interactive"` (default) or
|
|
11
|
+
//! `"static"`; `renderers` is a Hash of build-time renderer callables.
|
|
12
|
+
//!
|
|
13
|
+
//! The pure-Ruby wrapper in `lib/carve.rb` adds the keyword-argument form
|
|
14
|
+
//! `Carve.to_html(source, extensions: [...], mode: ..., renderers: {...})` on
|
|
15
|
+
//! top of these primitives.
|
|
16
|
+
|
|
17
|
+
mod ast_json;
|
|
18
|
+
|
|
19
|
+
use ast_json::document_to_json;
|
|
20
|
+
use carve_rs::{
|
|
21
|
+
Autolink, CarveExtension, Citations, CodeCallouts, Details, ExternalLinks, FencedRender,
|
|
22
|
+
HeadingPermalinks, ListTable, MathBlock, Mode, Options, Spoiler, StaticRenderers, TabNormalize,
|
|
23
|
+
TableOfContents,
|
|
24
|
+
Wikilinks,
|
|
25
|
+
};
|
|
26
|
+
use magnus::value::{InnerValue, Opaque};
|
|
27
|
+
use magnus::{function, prelude::*, Error, RArray, RHash, Ruby, Value};
|
|
28
|
+
|
|
29
|
+
/// HTML-escape a string for the renderer-failure fallback path.
|
|
30
|
+
///
|
|
31
|
+
/// carve-rs emits a *present* static renderer's return value verbatim (it is
|
|
32
|
+
/// the renderer's job to produce safe HTML). So when our Ruby wrapper has to
|
|
33
|
+
/// fall back to the construct source - because the callable raised or returned
|
|
34
|
+
/// a non-string - that source MUST be escaped here, or a source containing HTML
|
|
35
|
+
/// (e.g. `<img onerror=...>`) would be emitted raw. The no-renderer path inside
|
|
36
|
+
/// carve-rs already escapes its `<pre><code>` source block; this keeps the
|
|
37
|
+
/// failing-renderer floor equally safe rather than a raw-passthrough hole.
|
|
38
|
+
fn escape_html(s: &str) -> String {
|
|
39
|
+
let mut out = String::with_capacity(s.len());
|
|
40
|
+
for ch in s.chars() {
|
|
41
|
+
match ch {
|
|
42
|
+
'&' => out.push_str("&"),
|
|
43
|
+
'<' => out.push_str("<"),
|
|
44
|
+
'>' => out.push_str(">"),
|
|
45
|
+
'"' => out.push_str("""),
|
|
46
|
+
'\'' => out.push_str("'"),
|
|
47
|
+
_ => out.push(ch),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Build an owned, boxed extension instance from a Ruby-facing name.
|
|
54
|
+
///
|
|
55
|
+
/// Accepts both snake_case (`math_block`) and hyphenated (`math-block`) forms
|
|
56
|
+
/// so symbols and strings map cleanly. Returns `None` for an unknown name; the
|
|
57
|
+
/// caller turns that into a Ruby `ArgumentError`.
|
|
58
|
+
fn extension_for(name: &str) -> Option<Box<dyn CarveExtension>> {
|
|
59
|
+
// Normalize: lowercase, hyphens -> underscores, strip surrounding space.
|
|
60
|
+
let key = name.trim().to_ascii_lowercase().replace('-', "_");
|
|
61
|
+
let ext: Box<dyn CarveExtension> = match key.as_str() {
|
|
62
|
+
"autolink" => Box::new(Autolink::new()),
|
|
63
|
+
"details" => Box::new(Details::new()),
|
|
64
|
+
"list_table" | "listtable" => Box::new(ListTable::new()),
|
|
65
|
+
"math_block" | "mathblock" | "math" => Box::new(MathBlock::new()),
|
|
66
|
+
"heading_permalinks" | "permalinks" => Box::new(HeadingPermalinks::new()),
|
|
67
|
+
"citations" => Box::new(Citations::new()),
|
|
68
|
+
"code_callouts" | "codecallouts" => Box::new(CodeCallouts::new()),
|
|
69
|
+
"tab_normalize" | "tabnormalize" => Box::new(TabNormalize::new()),
|
|
70
|
+
"wikilinks" => Box::new(Wikilinks::new()),
|
|
71
|
+
"external_links" | "externallinks" => Box::new(ExternalLinks::new()),
|
|
72
|
+
// The mermaid preset carries the static-renderer key, so a static
|
|
73
|
+
// render can consult `renderers: {'mermaid' => ...}`. (Plain
|
|
74
|
+
// `FencedRender::new("text")`/`new("mermaid")` would degrade to source
|
|
75
|
+
// even with a renderer supplied, since it has no static-renderer key.)
|
|
76
|
+
// `fenced_render` now maps to the mermaid preset (was the no-key
|
|
77
|
+
// `text` claim before static mode) to match the carve-py sibling and
|
|
78
|
+
// expose a renderer-capable default; `mermaid` is an explicit alias.
|
|
79
|
+
"fenced_render" | "fencedrender" | "mermaid" => Box::new(FencedRender::mermaid()),
|
|
80
|
+
// Graphviz/DOT preset; its static path consults
|
|
81
|
+
// `renderers: {'graphviz' => ...}`, else degrades to the DOT source.
|
|
82
|
+
"fenced_render_graphviz" | "graphviz" | "dot" => Box::new(FencedRender::graphviz()),
|
|
83
|
+
// Chart.js preset (JSON mode); its static path consults
|
|
84
|
+
// `renderers: {'chart' => ...}`, else degrades to the JSON source.
|
|
85
|
+
"fenced_render_chart" | "chart" => Box::new(FencedRender::chart()),
|
|
86
|
+
"spoiler" => Box::new(Spoiler::new()),
|
|
87
|
+
"table_of_contents" | "tableofcontents" | "toc" => Box::new(TableOfContents::new()),
|
|
88
|
+
_ => return None,
|
|
89
|
+
};
|
|
90
|
+
Some(ext)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Map a Ruby-facing mode string to a carve-rs [`Mode`].
|
|
94
|
+
///
|
|
95
|
+
/// Rejects any unknown string with `ArgumentError`, mirroring the spec's
|
|
96
|
+
/// "MUST reject an unknown mode value" (no guessing) and the unknown-extension
|
|
97
|
+
/// error style. Omitting the mode in Ruby defaults to `"interactive"`, so
|
|
98
|
+
/// existing callers are unaffected.
|
|
99
|
+
fn parse_mode(ruby: &Ruby, mode: &str) -> Result<Mode, Error> {
|
|
100
|
+
match mode {
|
|
101
|
+
"interactive" => Ok(Mode::Interactive),
|
|
102
|
+
"static" => Ok(Mode::Static),
|
|
103
|
+
other => Err(Error::new(
|
|
104
|
+
ruby.exception_arg_error(),
|
|
105
|
+
format!(
|
|
106
|
+
"Unknown Carve render mode: {other:?} (supported: \"interactive\", \"static\")"
|
|
107
|
+
),
|
|
108
|
+
)),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// Invoke a stored Ruby callable's `call` method with `args`, returning its
|
|
113
|
+
/// String result or the HTML-escaped `fallback` on any failure.
|
|
114
|
+
///
|
|
115
|
+
/// "Any failure" = the callable raised, OR returned a value that is not a
|
|
116
|
+
/// String. Both degrade to the escaped fallback so a bad renderer never
|
|
117
|
+
/// produces blank output and can never inject raw HTML. The render runs
|
|
118
|
+
/// synchronously on the Ruby thread, so `Ruby::get()` succeeds here.
|
|
119
|
+
fn call_renderer<A>(callable: &Opaque<Value>, args: A, fallback: &str) -> String
|
|
120
|
+
where
|
|
121
|
+
A: magnus::ArgList,
|
|
122
|
+
{
|
|
123
|
+
let Ok(ruby) = Ruby::get() else {
|
|
124
|
+
// Not on a Ruby thread - cannot call back; degrade safely.
|
|
125
|
+
return escape_html(fallback);
|
|
126
|
+
};
|
|
127
|
+
let value: Value = callable.get_inner_with(&ruby);
|
|
128
|
+
match value.funcall::<_, _, String>("call", args) {
|
|
129
|
+
Ok(s) => s,
|
|
130
|
+
// Callable raised, or returned a non-String: escaped-source fallback.
|
|
131
|
+
Err(_) => escape_html(fallback),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Wrap a Ruby diagram callable `(String) -> String` into a carve-rs closure.
|
|
136
|
+
///
|
|
137
|
+
/// On a raising / non-string-returning callable it degrades to the
|
|
138
|
+
/// HTML-escaped source (see [`call_renderer`]).
|
|
139
|
+
fn wrap_diagram(callable: Opaque<Value>) -> Box<dyn Fn(&str) -> String + 'static> {
|
|
140
|
+
Box::new(move |src: &str| call_renderer(&callable, (src,), src))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Wrap a Ruby math callable `(String, bool) -> String` into a carve-rs
|
|
144
|
+
/// closure.
|
|
145
|
+
///
|
|
146
|
+
/// Same contract as [`wrap_diagram`] (including the HTML-escaped fallback), but
|
|
147
|
+
/// the callable receives the TeX source and a `display` flag (`true` for block
|
|
148
|
+
/// / display math, `false` for inline).
|
|
149
|
+
fn wrap_math(callable: Opaque<Value>) -> Box<dyn Fn(&str, bool) -> String + 'static> {
|
|
150
|
+
Box::new(move |tex: &str, display: bool| call_renderer(&callable, (tex, display), tex))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Build a [`StaticRenderers`] from a Ruby Hash of callables.
|
|
154
|
+
///
|
|
155
|
+
/// Recognized keys (String or Symbol): `"mermaid"` / `"chart"` / `"graphviz"`
|
|
156
|
+
/// (callables `(String) -> String`) and `"math"` (callable
|
|
157
|
+
/// `(String, bool) -> String`). Unknown keys raise `ArgumentError`. A missing
|
|
158
|
+
/// key leaves that renderer absent, so the matching static path degrades to
|
|
159
|
+
/// source.
|
|
160
|
+
fn build_renderers(ruby: &Ruby, hash: RHash) -> Result<StaticRenderers, Error> {
|
|
161
|
+
let mut out = StaticRenderers::default();
|
|
162
|
+
// Collect (key, value) pairs; RHash::each is not exposed, so use foreach.
|
|
163
|
+
let mut pairs: Vec<(String, Value)> = Vec::new();
|
|
164
|
+
hash.foreach(|key: Value, value: Value| {
|
|
165
|
+
// Accept both String and Symbol keys via to_string-style coercion.
|
|
166
|
+
let name: String = key.to_r_string()?.to_string()?;
|
|
167
|
+
pairs.push((name, value));
|
|
168
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
169
|
+
})?;
|
|
170
|
+
|
|
171
|
+
for (name, value) in pairs {
|
|
172
|
+
let callable: Opaque<Value> = Opaque::from(value);
|
|
173
|
+
match name.trim().to_ascii_lowercase().as_str() {
|
|
174
|
+
"mermaid" => out.mermaid = Some(wrap_diagram(callable)),
|
|
175
|
+
"chart" => out.chart = Some(wrap_diagram(callable)),
|
|
176
|
+
"graphviz" => out.graphviz = Some(wrap_diagram(callable)),
|
|
177
|
+
"math" => out.math = Some(wrap_math(callable)),
|
|
178
|
+
other => {
|
|
179
|
+
return Err(Error::new(
|
|
180
|
+
ruby.exception_arg_error(),
|
|
181
|
+
format!(
|
|
182
|
+
"Unknown Carve renderer key: {other:?} (supported: \"mermaid\", \"chart\", \"graphviz\", \"math\")"
|
|
183
|
+
),
|
|
184
|
+
));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
Ok(out)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Render Carve source to HTML with no extensions enabled.
|
|
192
|
+
fn to_html(source: String) -> String {
|
|
193
|
+
carve_rs::to_html(&source)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Parse Carve source and return its AST as a JSON string.
|
|
197
|
+
///
|
|
198
|
+
/// The pure-Ruby wrapper (`Carve.parse`) turns this into a tree of Ruby
|
|
199
|
+
/// Hashes/Arrays. Parsing uses the default profile (no extensions); the AST is
|
|
200
|
+
/// the raw parse tree, so render-stage extension rewrites are not applied.
|
|
201
|
+
fn to_ast_json(source: String) -> String {
|
|
202
|
+
document_to_json(&carve_rs::parse(&source))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Render Carve source to HTML with the named extensions enabled.
|
|
206
|
+
///
|
|
207
|
+
/// `names` is a Ruby Array of Strings/Symbols. An unrecognized name raises a
|
|
208
|
+
/// Ruby `ArgumentError`. Always interactive mode, no renderers.
|
|
209
|
+
fn to_html_with_extensions(ruby: &Ruby, source: String, names: RArray) -> Result<String, Error> {
|
|
210
|
+
let boxed = boxed_extensions(ruby, names)?;
|
|
211
|
+
let mut options = Options::new();
|
|
212
|
+
for ext in &boxed {
|
|
213
|
+
options = options.with_extension(ext.as_ref());
|
|
214
|
+
}
|
|
215
|
+
Ok(carve_rs::to_html_with_options(&source, &options))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Collect owned, boxed extension instances from a Ruby Array of names.
|
|
219
|
+
///
|
|
220
|
+
/// carve_rs::Options holds `&dyn CarveExtension` with a lifetime tied to the
|
|
221
|
+
/// caller's scope, so the boxes must outlive the Options + render call; the
|
|
222
|
+
/// caller keeps the returned Vec alive across both.
|
|
223
|
+
fn boxed_extensions(ruby: &Ruby, names: RArray) -> Result<Vec<Box<dyn CarveExtension>>, Error> {
|
|
224
|
+
let mut boxed: Vec<Box<dyn CarveExtension>> = Vec::with_capacity(names.len());
|
|
225
|
+
for item in names.into_iter() {
|
|
226
|
+
let name: String = item.to_r_string()?.to_string()?;
|
|
227
|
+
match extension_for(&name) {
|
|
228
|
+
Some(ext) => boxed.push(ext),
|
|
229
|
+
None => {
|
|
230
|
+
return Err(Error::new(
|
|
231
|
+
ruby.exception_arg_error(),
|
|
232
|
+
format!("Unknown Carve extension: {name:?}"),
|
|
233
|
+
));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
Ok(boxed)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Full static-render-mode primitive: extensions + mode + renderers.
|
|
241
|
+
///
|
|
242
|
+
/// * `names` - Ruby Array of extension name Strings/Symbols (may be empty).
|
|
243
|
+
/// * `mode` - `"interactive"` (default) or `"static"`; unknown raises
|
|
244
|
+
/// `ArgumentError`.
|
|
245
|
+
/// * `renderers` - Ruby Hash of build-time renderer callables (keys
|
|
246
|
+
/// `mermaid` / `chart` / `graphviz` -> `(String) -> String`, `math` ->
|
|
247
|
+
/// `(String, bool) -> String`), consulted only on the static HTML path.
|
|
248
|
+
fn to_html_full(
|
|
249
|
+
ruby: &Ruby,
|
|
250
|
+
source: String,
|
|
251
|
+
names: RArray,
|
|
252
|
+
mode: String,
|
|
253
|
+
renderers: RHash,
|
|
254
|
+
) -> Result<String, Error> {
|
|
255
|
+
let parsed_mode = parse_mode(ruby, &mode)?;
|
|
256
|
+
let static_renderers = build_renderers(ruby, renderers)?;
|
|
257
|
+
let boxed = boxed_extensions(ruby, names)?;
|
|
258
|
+
|
|
259
|
+
let mut options = Options::new()
|
|
260
|
+
.with_mode(parsed_mode)
|
|
261
|
+
.with_renderers(static_renderers);
|
|
262
|
+
for ext in &boxed {
|
|
263
|
+
options = options.with_extension(ext.as_ref());
|
|
264
|
+
}
|
|
265
|
+
Ok(carve_rs::to_html_with_options(&source, &options))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Entry point invoked by Ruby when the extension is loaded.
|
|
269
|
+
///
|
|
270
|
+
/// `name = "carve"` makes the macro emit the `Init_carve` symbol that matches
|
|
271
|
+
/// the compiled object `carve.so` (the [lib] name), even though the crate
|
|
272
|
+
/// package is named `carve-rb`.
|
|
273
|
+
#[magnus::init(name = "carve")]
|
|
274
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
275
|
+
let module = ruby.define_module("Carve")?;
|
|
276
|
+
// Native primitives. The pure-Ruby wrapper in lib/carve.rb defines the
|
|
277
|
+
// public `Carve.to_html(source, extensions:, mode:, renderers:)` on top of
|
|
278
|
+
// these. `_to_html` is the no-extension fast path; the wrapper owns the
|
|
279
|
+
// bare `to_html` name.
|
|
280
|
+
module.define_singleton_method("_to_html", function!(to_html, 1))?;
|
|
281
|
+
module.define_singleton_method("_to_ast_json", function!(to_ast_json, 1))?;
|
|
282
|
+
module.define_singleton_method(
|
|
283
|
+
"to_html_with_extensions",
|
|
284
|
+
function!(to_html_with_extensions, 2),
|
|
285
|
+
)?;
|
|
286
|
+
module.define_singleton_method("to_html_full", function!(to_html_full, 4))?;
|
|
287
|
+
Ok(())
|
|
288
|
+
}
|
data/lib/carve.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Carve: Ruby bindings for the Carve markup language.
|
|
4
|
+
#
|
|
5
|
+
# This file loads the compiled native extension (built from ext/carve, a Rust
|
|
6
|
+
# crate wrapping the carve-rs engine via magnus) and layers a small,
|
|
7
|
+
# idiomatic Ruby API on top of the raw primitives it exports.
|
|
8
|
+
require "json"
|
|
9
|
+
require_relative "carve/version"
|
|
10
|
+
|
|
11
|
+
# Load the compiled native extension. Built by `rake compile` to
|
|
12
|
+
# lib/carve/carve.so (or .bundle on macOS).
|
|
13
|
+
require_relative "carve/carve"
|
|
14
|
+
|
|
15
|
+
module Carve
|
|
16
|
+
# Extension names the native binding understands (snake_case or hyphenated).
|
|
17
|
+
EXTENSIONS = %i[
|
|
18
|
+
autolink
|
|
19
|
+
details
|
|
20
|
+
list_table
|
|
21
|
+
math_block
|
|
22
|
+
heading_permalinks
|
|
23
|
+
citations
|
|
24
|
+
code_callouts
|
|
25
|
+
tab_normalize
|
|
26
|
+
wikilinks
|
|
27
|
+
external_links
|
|
28
|
+
fenced_render
|
|
29
|
+
fenced_render_graphviz
|
|
30
|
+
fenced_render_chart
|
|
31
|
+
spoiler
|
|
32
|
+
table_of_contents
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# Render modes the native binding understands.
|
|
36
|
+
#
|
|
37
|
+
# +:interactive+ (default) emits live HTML with client-script hooks (e.g.
|
|
38
|
+
# `<pre class="mermaid">`, `<details>`). +:static+ emits self-contained HTML
|
|
39
|
+
# for print / PDF / archival: it forces disclosure (`<details open>`) and
|
|
40
|
+
# pre-renders client-script constructs through the +renderers:+ callables,
|
|
41
|
+
# degrading to (escaped) source when a renderer is absent or fails.
|
|
42
|
+
MODES = %i[interactive static].freeze
|
|
43
|
+
|
|
44
|
+
# Renderer keys accepted by the +renderers:+ Hash (see .to_html). Each maps a
|
|
45
|
+
# construct's source to a self-contained HTML string emitted on the static
|
|
46
|
+
# path. +mermaid+ / +chart+ / +graphviz+ are callables `(String) -> String`;
|
|
47
|
+
# +math+ is `(String, display_bool) -> String`.
|
|
48
|
+
RENDERER_KEYS = %i[mermaid chart graphviz math].freeze
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
# Render Carve +source+ to an HTML string.
|
|
52
|
+
#
|
|
53
|
+
# With no extensions:
|
|
54
|
+
# Carve.to_html("# Hello") # => "<section ...>\n <h1>Hello</h1>..."
|
|
55
|
+
#
|
|
56
|
+
# With extensions (Array of names as Symbols or Strings):
|
|
57
|
+
# Carve.to_html(src, extensions: [:math_block])
|
|
58
|
+
# Carve.to_html(src, extensions: %w[math-block list-table])
|
|
59
|
+
#
|
|
60
|
+
# Recognized extension names: see Carve::EXTENSIONS. Names may be given
|
|
61
|
+
# snake_case (:math_block) or hyphenated ("math-block").
|
|
62
|
+
#
|
|
63
|
+
# An unknown extension name raises ArgumentError (from the native layer).
|
|
64
|
+
#
|
|
65
|
+
# ==== Static render mode
|
|
66
|
+
#
|
|
67
|
+
# Pass +mode: :static+ (or "static") to emit self-contained HTML for print /
|
|
68
|
+
# PDF / archival. In static mode disclosure is forced (`<details open>`) and
|
|
69
|
+
# client-script constructs are pre-rendered through the +renderers:+ Hash:
|
|
70
|
+
#
|
|
71
|
+
# Carve.to_html(src, extensions: [:fenced_render], mode: :static,
|
|
72
|
+
# renderers: { mermaid: ->(s) { "<svg>#{s}</svg>" } })
|
|
73
|
+
#
|
|
74
|
+
# Renderer callables (Symbol or String keys, see Carve::RENDERER_KEYS):
|
|
75
|
+
# * +:mermaid+ / +:chart+ / +:graphviz+ -> callable `(String) -> String`
|
|
76
|
+
# * +:math+ -> callable `(String, display) -> String`
|
|
77
|
+
#
|
|
78
|
+
# When a needed renderer is absent, or a renderer raises / returns a
|
|
79
|
+
# non-String, the construct degrades to its HTML-ESCAPED source (never blank,
|
|
80
|
+
# never raw HTML). Omitting +mode:+ defaults to interactive (non-breaking).
|
|
81
|
+
#
|
|
82
|
+
# An unknown mode or renderer key raises ArgumentError (from the native
|
|
83
|
+
# layer).
|
|
84
|
+
def to_html(source, extensions: nil, mode: nil, renderers: nil)
|
|
85
|
+
list = Array(extensions)
|
|
86
|
+
|
|
87
|
+
# Fast path: interactive (default), no extensions, no renderers.
|
|
88
|
+
if list.empty? && (mode.nil? || mode.to_s == "interactive") && (renderers.nil? || renderers.empty?)
|
|
89
|
+
return _to_html(source.to_s)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
to_html_full(
|
|
93
|
+
source.to_s,
|
|
94
|
+
list.map(&:to_s),
|
|
95
|
+
(mode || :interactive).to_s,
|
|
96
|
+
renderers || {},
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Parse Carve +source+ into an AST: a tree of Ruby Hashes and Arrays.
|
|
101
|
+
#
|
|
102
|
+
# Carve.parse("# Hi")
|
|
103
|
+
# # => {type: "document", frontmatter: {}, footnote_defs: {},
|
|
104
|
+
# # children: [{type: "heading", level: 1,
|
|
105
|
+
# # children: [{type: "text", value: "Hi"}], attrs: nil}],
|
|
106
|
+
# # source_len: 4}
|
|
107
|
+
#
|
|
108
|
+
# Every node is a Hash with a +:type+ key plus its fields; child collections
|
|
109
|
+
# are Arrays; +:attrs+ is +nil+ or a Hash of +{id:, classes:, key_values:}+.
|
|
110
|
+
# Keys are symbols. This is the raw parse tree (default profile, no
|
|
111
|
+
# extensions), suitable for a custom renderer (e.g. Carve -> PDF).
|
|
112
|
+
def parse(source)
|
|
113
|
+
# max_nesting: false - the engine already bounds nesting (its own
|
|
114
|
+
# MAX_NESTING_DEPTH cap), and that cap exceeds Ruby JSON's default
|
|
115
|
+
# max_nesting of 100. Without this, deeply-nested-but-valid documents
|
|
116
|
+
# that Carve.to_html renders fine would raise JSON::NestingError here.
|
|
117
|
+
JSON.parse(_to_ast_json(source.to_s), symbolize_names: true, max_nesting: false)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: carve-lang
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- markup-carve
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rb_sys
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.9'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.9'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake-compiler
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.2'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.2'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: minitest
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '5.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '5.0'
|
|
69
|
+
description: |-
|
|
70
|
+
Parse and render Carve markup to HTML from Ruby. A native extension
|
|
71
|
+
(magnus + rb-sys) over the carve-rs engine, mirroring how Djot's djotter
|
|
72
|
+
gem wraps the jotdown crate. No parser is reimplemented in Ruby.
|
|
73
|
+
email:
|
|
74
|
+
executables: []
|
|
75
|
+
extensions:
|
|
76
|
+
- ext/carve/extconf.rb
|
|
77
|
+
extra_rdoc_files: []
|
|
78
|
+
files:
|
|
79
|
+
- CHANGELOG.md
|
|
80
|
+
- LICENSE
|
|
81
|
+
- README.md
|
|
82
|
+
- ext/carve/Cargo.lock
|
|
83
|
+
- ext/carve/Cargo.toml
|
|
84
|
+
- ext/carve/extconf.rb
|
|
85
|
+
- ext/carve/src/ast_json.rs
|
|
86
|
+
- ext/carve/src/lib.rs
|
|
87
|
+
- lib/carve.rb
|
|
88
|
+
- lib/carve/version.rb
|
|
89
|
+
homepage: https://github.com/markup-carve/carve-rb
|
|
90
|
+
licenses:
|
|
91
|
+
- MIT
|
|
92
|
+
metadata: {}
|
|
93
|
+
post_install_message:
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: 3.0.0
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 3.4.19
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: Native Ruby bindings for the Carve markup language.
|
|
112
|
+
test_files: []
|