fulgur_chart 0.3.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/README.md +112 -0
- data/ext/fulgur_chart/Cargo.toml +15 -0
- data/ext/fulgur_chart/extconf.rb +4 -0
- data/ext/fulgur_chart/src/lib.rs +255 -0
- data/lib/fulgur_chart.rb +89 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1feeff546e3edbf804f27aaf4e51fb50d1da3297286064a58e1775e36110bcfb
|
|
4
|
+
data.tar.gz: 89ee391afe75cd21d7f60ed05b533e00b280a3eea21e66f99369dc8f9f4a1d69
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bfe60580a7c4781031e81a20a74128b1d0d19fc421899703e61f0f44db9f76a448e9aa64440e4d9b69470d89b6ed599295b432928f1258748e28e1986a5913fa
|
|
7
|
+
data.tar.gz: 2e2d40dbeaefe0460c6c6703922c4f480b9a0903a395468d05d7c9c71ce41e0884947e1897e86cf40fbde8025b4ecd653d616f87d61ce20cff64e739922901f8
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# fulgur_chart (Ruby)
|
|
2
|
+
|
|
3
|
+
Ruby binding for [fulgur-chart](https://github.com/fulgur-rs/fulgur-chart) — render
|
|
4
|
+
chart.js v4 / Vega-Lite JSON specs to deterministic SVG/PNG via a Rust native extension
|
|
5
|
+
([magnus](https://github.com/matsadler/magnus) / [rb-sys](https://github.com/oxidize-rb/rb-sys)).
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Ruby >= 3.0
|
|
10
|
+
- A Rust toolchain (`cargo`) — the gem builds a native extension at install time.
|
|
11
|
+
|
|
12
|
+
## Build / test from source
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
cd crates/bindings/ruby
|
|
16
|
+
bundle install
|
|
17
|
+
bundle exec rake # compile the native extension + run the test suite
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`bundle exec rake` runs the `compile` task (which builds the Rust extension) followed by
|
|
21
|
+
the minitest suite.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
The API is a fluent **builder**: `FulgurChart.build(spec)` returns a builder you configure with
|
|
26
|
+
chainable setters and finish with `render(:svg)` / `render(:png)`.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
require "fulgur_chart"
|
|
30
|
+
|
|
31
|
+
spec = <<~JSON
|
|
32
|
+
{
|
|
33
|
+
"type": "bar",
|
|
34
|
+
"data": {
|
|
35
|
+
"labels": ["a", "b", "c"],
|
|
36
|
+
"datasets": [{ "data": [1, 3, 2] }]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
JSON
|
|
40
|
+
|
|
41
|
+
# SVG (UTF-8 String)
|
|
42
|
+
svg = FulgurChart.build(spec).render(:svg)
|
|
43
|
+
File.write("chart.svg", svg)
|
|
44
|
+
|
|
45
|
+
# PNG (binary / ASCII-8BIT String) — write with binwrite to avoid encoding mangling
|
|
46
|
+
png = FulgurChart.build(spec).width(800).height(600).scale(2.0).render(:png)
|
|
47
|
+
File.binwrite("chart.png", png)
|
|
48
|
+
|
|
49
|
+
# Set a default format with .format, then call render with no argument
|
|
50
|
+
png2 = FulgurChart.build(spec).format(:png).render
|
|
51
|
+
|
|
52
|
+
# The builder is reusable and reconfigurable between renders
|
|
53
|
+
chart = FulgurChart.build(spec).dsl(:chartjs)
|
|
54
|
+
a = chart.width(400).render(:svg)
|
|
55
|
+
b = chart.width(1234).render(:svg)
|
|
56
|
+
|
|
57
|
+
# JSON Schema for a DSL (compact JSON String)
|
|
58
|
+
chartjs_schema = FulgurChart.schema(:chartjs)
|
|
59
|
+
vegalite_schema = FulgurChart.schema("vegalite")
|
|
60
|
+
|
|
61
|
+
# Library version (String, e.g. "0.1.0")
|
|
62
|
+
FulgurChart.version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The DSL is auto-detected from the spec: a top-level `mark` key selects Vega-Lite, a
|
|
66
|
+
top-level `type` key selects chart.js. Use `.dsl(:chartjs)` / `.dsl(:vegalite)` to override.
|
|
67
|
+
Options accept either a Symbol or a String (`.dsl(:chartjs)` == `.dsl("chartjs")`).
|
|
68
|
+
|
|
69
|
+
### API
|
|
70
|
+
|
|
71
|
+
| Method | Returns |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `FulgurChart.build(spec_json)` | a `FulgurChart::Builder` |
|
|
74
|
+
| `builder.render(format = nil)` | `String` — `:svg` → UTF-8, `:png` → binary (ASCII-8BIT). Format precedence: argument > `.format()` > default `:svg` |
|
|
75
|
+
| `FulgurChart.render(spec_json, format, **opts)` | low-level primitive the builder calls; same return contract |
|
|
76
|
+
| `FulgurChart.schema(dsl)` | JSON Schema `String` for `:chartjs` or `:vegalite` |
|
|
77
|
+
| `FulgurChart.version` | version `String` |
|
|
78
|
+
|
|
79
|
+
### Builder setters
|
|
80
|
+
|
|
81
|
+
Each setter returns the builder for chaining; all are optional.
|
|
82
|
+
|
|
83
|
+
| Setter | Type | Notes |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `.width(w)` / `.height(h)` | Float | Canvas size override (applied before input-limit validation). |
|
|
86
|
+
| `.scale(s)` | Float | Raster scale factor; raster output only (ignored when rendering `:svg`). Default `1.0`. |
|
|
87
|
+
| `.strict` / `.strict(bool)` | Bool | Reject unknown keys (raises `StrictError`). `.strict` ⇒ `true`. Default `false`. |
|
|
88
|
+
| `.dsl(d)` | `:chartjs` \| `:vegalite` (Symbol/String) | Override DSL auto-detection. |
|
|
89
|
+
| `.font(bytes)` | binary `String` | A TTF/OTF font to use instead of the bundled default (Noto Sans JP). |
|
|
90
|
+
| `.format(f)` | `:svg` \| `:png` (Symbol/String) | Default format for a terminal `render` with no argument. |
|
|
91
|
+
|
|
92
|
+
### Errors
|
|
93
|
+
|
|
94
|
+
The error hierarchy lives in the native extension under the `FulgurChart` module
|
|
95
|
+
(the module is `FulgurChart`, not `Fulgur`, to avoid a top-level collision with the
|
|
96
|
+
Fulgur PDF library when both gems are loaded in the same process):
|
|
97
|
+
|
|
98
|
+
- `FulgurChart::ParseError < StandardError` — invalid JSON, undetectable DSL, unknown format,
|
|
99
|
+
input-limit violations.
|
|
100
|
+
- `FulgurChart::StrictError < FulgurChart::ParseError` — unknown key encountered under `.strict`.
|
|
101
|
+
- `FulgurChart::RenderError < StandardError` — raster rendering failure.
|
|
102
|
+
|
|
103
|
+
Note the font-error asymmetry: an invalid font raises `FulgurChart::ParseError` when rendering
|
|
104
|
+
`:svg` but `FulgurChart::RenderError` when rendering `:png`, because the two outputs go through
|
|
105
|
+
different render pipelines.
|
|
106
|
+
|
|
107
|
+
## Note: packaging
|
|
108
|
+
|
|
109
|
+
The published-gem story — source-installing the path-dependent Rust core and shipping
|
|
110
|
+
cross-platform prebuilt gems — is tracked as a follow-up. Today the gem builds against the
|
|
111
|
+
in-repo core via a Cargo path dependency, so it is intended for use from within this
|
|
112
|
+
repository (build from source as shown above).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "fulgur_chart"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
publish = false
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["cdylib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
magnus = "0.7"
|
|
12
|
+
fulgur-chart = "0.3.0"
|
|
13
|
+
serde = { version = "1", features = ["derive"] }
|
|
14
|
+
serde_json = "1"
|
|
15
|
+
schemars = { version = "1.2", features = ["preserve_order"] }
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
use fulgur_chart::guard::{validate_spec, InputLimits};
|
|
2
|
+
use magnus::{
|
|
3
|
+
function,
|
|
4
|
+
prelude::*,
|
|
5
|
+
scan_args::{get_kwargs, scan_args},
|
|
6
|
+
Error, ExceptionClass, RHash, RString, Ruby, Value,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// --- error helpers (classification is by CALL SITE, never by parsing the message) ---
|
|
10
|
+
|
|
11
|
+
fn exc_class(ruby: &Ruby, name: &str) -> ExceptionClass {
|
|
12
|
+
let module = ruby
|
|
13
|
+
.define_module("FulgurChart")
|
|
14
|
+
.expect("FulgurChart module defined in init");
|
|
15
|
+
module
|
|
16
|
+
.const_get::<_, ExceptionClass>(name)
|
|
17
|
+
.expect("error class defined in init")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn parse_err(ruby: &Ruby, msg: impl Into<String>) -> Error {
|
|
21
|
+
Error::new(exc_class(ruby, "ParseError"), msg.into())
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn strict_err(ruby: &Ruby, msg: impl Into<String>) -> Error {
|
|
25
|
+
Error::new(exc_class(ruby, "StrictError"), msg.into())
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The image path classifies raster errors as RenderError (asymmetry vs the SVG path,
|
|
29
|
+
// which maps font/render failures to ParseError).
|
|
30
|
+
fn render_err(ruby: &Ruby, msg: impl Into<String>) -> Error {
|
|
31
|
+
Error::new(exc_class(ruby, "RenderError"), msg.into())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Coerce a Ruby argument to a String, accepting both String and Symbol (idiomatic Ruby lets
|
|
35
|
+
/// callers pass `dsl: :chartjs` / `format: :png`). magnus's String conversion rejects Symbols,
|
|
36
|
+
/// so without this `to_s` coercion a symbol would raise TypeError instead of being accepted.
|
|
37
|
+
fn coerce_string(v: Value) -> Result<String, Error> {
|
|
38
|
+
v.funcall("to_s", ())
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- DSL detection + parse (mirrors fulgur-chart-cli `detect_dsl` / `parse_spec`) ---
|
|
42
|
+
|
|
43
|
+
/// Lightweight serde helper that only deserialises the top-level keys used for DSL detection.
|
|
44
|
+
#[derive(serde::Deserialize)]
|
|
45
|
+
struct DslDetector {
|
|
46
|
+
mark: Option<serde::de::IgnoredAny>,
|
|
47
|
+
#[serde(rename = "type")]
|
|
48
|
+
r#type: Option<serde::de::IgnoredAny>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Infer DSL from spec JSON: `mark` key → vegalite, `type` key → chartjs, neither → Err.
|
|
52
|
+
fn detect_dsl(json: &str) -> Result<&'static str, String> {
|
|
53
|
+
let d: DslDetector = serde_json::from_str(json).map_err(|e| format!("invalid JSON: {e}"))?;
|
|
54
|
+
if d.mark.is_some() {
|
|
55
|
+
return Ok("vegalite");
|
|
56
|
+
}
|
|
57
|
+
if d.r#type.is_some() {
|
|
58
|
+
return Ok("chartjs");
|
|
59
|
+
}
|
|
60
|
+
Err("cannot auto-detect DSL: specify dsl: 'chartjs' or 'vegalite'".to_string())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Parse a spec JSON string to IR using the specified DSL (chartjs or vegalite).
|
|
64
|
+
fn parse_spec(json: &str, dsl: &str, strict: bool) -> Result<fulgur_chart::ir::ChartSpec, String> {
|
|
65
|
+
match dsl {
|
|
66
|
+
"vegalite" => fulgur_chart::frontend::vegalite::parse(json, strict),
|
|
67
|
+
_ => fulgur_chart::frontend::chartjs::parse(json, strict), // "chartjs"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- RenderOptions ---
|
|
72
|
+
|
|
73
|
+
#[derive(Default)]
|
|
74
|
+
struct Opts {
|
|
75
|
+
width: Option<f64>,
|
|
76
|
+
height: Option<f64>,
|
|
77
|
+
// Consumed by the image path (render_chart_to_png); the SVG path ignores scale.
|
|
78
|
+
scale: f32,
|
|
79
|
+
strict: bool,
|
|
80
|
+
dsl: Option<String>,
|
|
81
|
+
font: Option<Vec<u8>>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Parse optional RenderOptions from the kwargs hash. Tolerates extra keys (e.g. `format:`
|
|
85
|
+
/// passed by render_image in Task 3) via the trailing `RHash` splat, so unknown keys do not
|
|
86
|
+
/// raise here.
|
|
87
|
+
fn parse_opts(ruby: &Ruby, kw: RHash) -> Result<Opts, Error> {
|
|
88
|
+
let args = get_kwargs::<
|
|
89
|
+
_,
|
|
90
|
+
(),
|
|
91
|
+
(
|
|
92
|
+
Option<f64>,
|
|
93
|
+
Option<f64>,
|
|
94
|
+
Option<f64>,
|
|
95
|
+
Option<bool>,
|
|
96
|
+
Option<Value>,
|
|
97
|
+
Option<RString>,
|
|
98
|
+
),
|
|
99
|
+
RHash, // splat: collect + ignore unknown keys
|
|
100
|
+
>(
|
|
101
|
+
kw,
|
|
102
|
+
&[],
|
|
103
|
+
&["width", "height", "scale", "strict", "dsl", "font"],
|
|
104
|
+
)?;
|
|
105
|
+
let (width, height, scale, strict, dsl_val, font) = args.optional;
|
|
106
|
+
|
|
107
|
+
// Accept String or Symbol for `dsl`; an explicit nil arrives as None (→ auto-detect).
|
|
108
|
+
let dsl = match dsl_val {
|
|
109
|
+
Some(v) => {
|
|
110
|
+
let d = coerce_string(v)?;
|
|
111
|
+
if d != "chartjs" && d != "vegalite" {
|
|
112
|
+
return Err(parse_err(ruby, format!("unsupported DSL '{d}'")));
|
|
113
|
+
}
|
|
114
|
+
Some(d)
|
|
115
|
+
}
|
|
116
|
+
None => None,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Copy the font bytes out of the Ruby string immediately; the borrow is unsafe and must
|
|
120
|
+
// not outlive any subsequent VM allocation.
|
|
121
|
+
let font = font.map(|s| unsafe { s.as_slice().to_vec() });
|
|
122
|
+
|
|
123
|
+
Ok(Opts {
|
|
124
|
+
width,
|
|
125
|
+
height,
|
|
126
|
+
scale: scale.map(|s| s as f32).unwrap_or(1.0),
|
|
127
|
+
strict: strict.unwrap_or(false),
|
|
128
|
+
dsl,
|
|
129
|
+
font,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Build and validate the IR, mirroring the processing order of `render_one`.
|
|
134
|
+
/// Reusable by render_svg / render_image (Task 3) / schema-less paths.
|
|
135
|
+
fn build_ir(
|
|
136
|
+
ruby: &Ruby,
|
|
137
|
+
spec_json: &str,
|
|
138
|
+
opts: &Opts,
|
|
139
|
+
) -> Result<fulgur_chart::ir::ChartSpec, Error> {
|
|
140
|
+
// 1. Resolve DSL: explicit opts.dsl OR auto-detect.
|
|
141
|
+
let dsl: &str = match &opts.dsl {
|
|
142
|
+
Some(d) => d.as_str(),
|
|
143
|
+
None => detect_dsl(spec_json).map_err(|e| parse_err(ruby, e))?,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// 2. Parse NON-strict → IR (render from this).
|
|
147
|
+
// Contract §3: propagate the core's error String verbatim. The exception class — not a
|
|
148
|
+
// message prefix — conveys parse/strict/render, so no CLI-style "error: ..." decoration.
|
|
149
|
+
let mut ir = parse_spec(spec_json, dsl, false).map_err(|e| parse_err(ruby, e))?;
|
|
150
|
+
|
|
151
|
+
// 3. If strict, re-parse with strict=true (discard IR; unknown key → StrictError).
|
|
152
|
+
if opts.strict {
|
|
153
|
+
parse_spec(spec_json, dsl, true).map_err(|e| strict_err(ruby, e))?;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. Apply width/height overrides BEFORE guard.
|
|
157
|
+
if let Some(w) = opts.width {
|
|
158
|
+
ir.width = w;
|
|
159
|
+
}
|
|
160
|
+
if let Some(h) = opts.height {
|
|
161
|
+
ir.height = h;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 5. Guard (failure → ParseError).
|
|
165
|
+
validate_spec(&ir, &InputLimits::default()).map_err(|e| parse_err(ruby, e))?;
|
|
166
|
+
|
|
167
|
+
Ok(ir)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- public API: render (low-level primitive; the FulgurChart::Builder is the intended API) ---
|
|
171
|
+
|
|
172
|
+
/// `FulgurChart.render(spec_json, format, **opts)` → String.
|
|
173
|
+
///
|
|
174
|
+
/// `format` is "svg" (→ UTF-8 String) or "png" (→ binary/ASCII-8BIT String), as a String or
|
|
175
|
+
/// Symbol. Unknown format → ParseError. `opts` are the RenderOptions kwargs
|
|
176
|
+
/// (width/height/scale/strict/dsl/font). Driven by `FulgurChart::Builder#render`, but also
|
|
177
|
+
/// callable directly: `FulgurChart.render(spec, :png, width: 800)`.
|
|
178
|
+
fn render(ruby: &Ruby, args: &[Value]) -> Result<RString, Error> {
|
|
179
|
+
let scanned = scan_args::<(String, Value), (), (), (), RHash, ()>(args)?;
|
|
180
|
+
let (spec_json, format_val) = scanned.required;
|
|
181
|
+
let format = coerce_string(format_val)?; // accept String or Symbol
|
|
182
|
+
let opts = parse_opts(ruby, scanned.keywords)?;
|
|
183
|
+
let ir = build_ir(ruby, &spec_json, &opts)?;
|
|
184
|
+
|
|
185
|
+
match format.as_str() {
|
|
186
|
+
"svg" => {
|
|
187
|
+
// Font present → render_chart_with_font (Err → ParseError on the SVG path);
|
|
188
|
+
// else the bundled-font render.
|
|
189
|
+
let svg = match &opts.font {
|
|
190
|
+
Some(bytes) => fulgur_chart::render::render_chart_with_font(&ir, bytes)
|
|
191
|
+
.map_err(|e| parse_err(ruby, e))?,
|
|
192
|
+
None => fulgur_chart::render::render_chart(&ir),
|
|
193
|
+
};
|
|
194
|
+
Ok(ruby.str_new(&svg)) // UTF-8 String
|
|
195
|
+
}
|
|
196
|
+
"png" => {
|
|
197
|
+
let fb: &[u8] = opts
|
|
198
|
+
.font
|
|
199
|
+
.as_deref()
|
|
200
|
+
.unwrap_or(fulgur_chart::font::DEFAULT_FONT);
|
|
201
|
+
// Invalid font on the image path → RenderError (the SVG path maps this to ParseError).
|
|
202
|
+
let png = fulgur_chart::raster_direct::render_chart_to_png(&ir, opts.scale, fb)
|
|
203
|
+
.map_err(|e| render_err(ruby, e))?;
|
|
204
|
+
Ok(ruby.str_from_slice(&png)) // ASCII-8BIT (BINARY) String
|
|
205
|
+
}
|
|
206
|
+
other => Err(parse_err(
|
|
207
|
+
ruby,
|
|
208
|
+
format!("unsupported format '{other}' (supported: svg, png)"),
|
|
209
|
+
)),
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- public API: schema ---
|
|
214
|
+
|
|
215
|
+
/// Return the JSON Schema (compact JSON String) for the given DSL (String or Symbol).
|
|
216
|
+
/// Mirrors the CLI's `run_schema`; unknown DSL → ParseError (consistent with `parse_opts`).
|
|
217
|
+
fn schema(ruby: &Ruby, dsl: Value) -> Result<String, Error> {
|
|
218
|
+
let dsl = coerce_string(dsl)?;
|
|
219
|
+
let s = match dsl.as_str() {
|
|
220
|
+
"chartjs" => schemars::schema_for!(fulgur_chart::schema::ChartJsSpec),
|
|
221
|
+
"vegalite" => schemars::schema_for!(fulgur_chart::schema::VegaLiteSpec),
|
|
222
|
+
other => {
|
|
223
|
+
return Err(parse_err(
|
|
224
|
+
ruby,
|
|
225
|
+
format!("unsupported DSL '{other}' (supported: chartjs, vegalite)"),
|
|
226
|
+
))
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
serde_json::to_string(&s).map_err(|e| render_err(ruby, format!("schema serialization: {e}")))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn version() -> String {
|
|
233
|
+
fulgur_chart::version().to_string()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#[magnus::init]
|
|
237
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
238
|
+
// Module name is `FulgurChart` (NOT `Fulgur`): a top-level `Fulgur` would collide with the
|
|
239
|
+
// Fulgur PDF library if both gems are loaded in the same process.
|
|
240
|
+
let module = ruby.define_module("FulgurChart")?;
|
|
241
|
+
|
|
242
|
+
// Canonical error hierarchy (single source of truth). lib/fulgur_chart.rb does not redefine
|
|
243
|
+
// these or alias anything.
|
|
244
|
+
let std_err = ruby.exception_standard_error();
|
|
245
|
+
let parse = module.define_error("ParseError", std_err)?;
|
|
246
|
+
module.define_error("StrictError", parse)?;
|
|
247
|
+
module.define_error("RenderError", std_err)?;
|
|
248
|
+
|
|
249
|
+
module.define_module_function("version", function!(version, 0))?;
|
|
250
|
+
module.define_module_function("schema", function!(schema, 1))?;
|
|
251
|
+
// Low-level render primitive; the FulgurChart::Builder (FulgurChart.build(...)) is the
|
|
252
|
+
// intended API and calls this under the hood.
|
|
253
|
+
module.define_module_function("render", function!(render, -1))?;
|
|
254
|
+
Ok(())
|
|
255
|
+
}
|
data/lib/fulgur_chart.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Native extension. Defines the FulgurChart module with:
|
|
4
|
+
# - FulgurChart.schema(dsl), FulgurChart.version
|
|
5
|
+
# - FulgurChart.render(spec_json, format, **opts) (low-level render primitive; the builder
|
|
6
|
+
# below is the intended API, but render is also callable directly)
|
|
7
|
+
# - errors: FulgurChart::ParseError / StrictError / RenderError
|
|
8
|
+
# Module name is `FulgurChart` (NOT `Fulgur`) to avoid a top-level collision with the
|
|
9
|
+
# Fulgur (PDF) library when both gems are loaded in the same process.
|
|
10
|
+
require_relative "fulgur_chart/fulgur_chart"
|
|
11
|
+
|
|
12
|
+
module FulgurChart
|
|
13
|
+
# Entry point for the builder API:
|
|
14
|
+
#
|
|
15
|
+
# FulgurChart.build(spec_json).width(800).dsl(:chartjs).render(:svg) # => String
|
|
16
|
+
# FulgurChart.build(spec_json).format(:png).render # => binary String
|
|
17
|
+
#
|
|
18
|
+
# `spec_json` is a chart.js v4 / Vega-Lite DSL JSON string. The behavior (DSL auto-detect,
|
|
19
|
+
# options, error classes, determinism) follows docs/binding-api-contract.md.
|
|
20
|
+
def self.build(spec_json)
|
|
21
|
+
Builder.new(spec_json)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Fluent, reusable builder. Setters mutate and return self; `render` may be called multiple
|
|
25
|
+
# times and the builder may be reconfigured between calls.
|
|
26
|
+
class Builder
|
|
27
|
+
def initialize(spec_json)
|
|
28
|
+
@spec = spec_json
|
|
29
|
+
@opts = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Override the chart width / height (px). Applied before input-limit validation.
|
|
33
|
+
def width(value)
|
|
34
|
+
set(:width, value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def height(value)
|
|
38
|
+
set(:height, value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Raster scale factor (ignored when rendering SVG).
|
|
42
|
+
def scale(value)
|
|
43
|
+
set(:scale, value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Force the input DSL ("chartjs"/"vegalite" or the matching Symbol). Omit to auto-detect.
|
|
47
|
+
def dsl(value)
|
|
48
|
+
set(:dsl, value)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# TrueType/OpenType font bytes (binary String). Omit to use the bundled Noto Sans JP.
|
|
52
|
+
def font(bytes)
|
|
53
|
+
set(:font, bytes)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Reject unknown keys. `strict` => true; `strict(false)` => false.
|
|
57
|
+
def strict(value = true)
|
|
58
|
+
set(:strict, value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Default output format for a terminal `render` with no argument (Symbol/String).
|
|
62
|
+
def format(value)
|
|
63
|
+
set(:format, value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Render to the given format ("svg"/"png" or the matching Symbol). Format precedence:
|
|
67
|
+
# explicit argument > `.format()` setter > default :svg. Returns a UTF-8 String for svg
|
|
68
|
+
# and a binary (ASCII-8BIT) String for png.
|
|
69
|
+
def render(fmt = nil)
|
|
70
|
+
# Distinguish "no explicit format" (nil) from a falsy-but-explicit one (e.g. false).
|
|
71
|
+
# An explicit argument always wins per the documented precedence; an invalid value is
|
|
72
|
+
# forwarded to the native layer to raise ParseError rather than silently falling back.
|
|
73
|
+
resolved =
|
|
74
|
+
if fmt.nil?
|
|
75
|
+
@opts.key?(:format) ? @opts[:format] : :svg
|
|
76
|
+
else
|
|
77
|
+
fmt
|
|
78
|
+
end
|
|
79
|
+
FulgurChart.render(@spec, resolved, **@opts.reject { |key, _| key == :format })
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def set(key, value)
|
|
85
|
+
@opts[key] = value
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fulgur_chart
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fulgur
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-21 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
|
+
description: Render chart.js / Vega-Lite specs to deterministic SVG/PNG (Rust core)
|
|
28
|
+
email:
|
|
29
|
+
executables: []
|
|
30
|
+
extensions:
|
|
31
|
+
- ext/fulgur_chart/extconf.rb
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- README.md
|
|
35
|
+
- ext/fulgur_chart/Cargo.toml
|
|
36
|
+
- ext/fulgur_chart/extconf.rb
|
|
37
|
+
- ext/fulgur_chart/src/lib.rs
|
|
38
|
+
- lib/fulgur_chart.rb
|
|
39
|
+
homepage: https://github.com/fulgur-rs/fulgur-chart
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT OR Apache-2.0
|
|
42
|
+
metadata:
|
|
43
|
+
homepage_uri: https://github.com/fulgur-rs/fulgur-chart
|
|
44
|
+
source_code_uri: https://github.com/fulgur-rs/fulgur-chart
|
|
45
|
+
changelog_uri: https://github.com/fulgur-rs/fulgur-chart/blob/main/crates/fulgur-chart/CHANGELOG.md
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.5.22
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Render chart.js / Vega-Lite specs to deterministic SVG/PNG (Rust core)
|
|
65
|
+
test_files: []
|