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.
@@ -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("&amp;"),
43
+ '<' => out.push_str("&lt;"),
44
+ '>' => out.push_str("&gt;"),
45
+ '"' => out.push_str("&quot;"),
46
+ '\'' => out.push_str("&#39;"),
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
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Carve
4
+ VERSION = "0.1.0"
5
+ end
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: []