carve-lang 0.1.3 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +41 -1
- data/README.md +58 -1
- data/ext/carve/Cargo.lock +3 -3
- data/ext/carve/Cargo.toml +2 -2
- data/ext/carve/src/lib.rs +247 -17
- data/lib/carve/version.rb +1 -1
- data/lib/carve.rb +92 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8f7b8390c9d7d5e50b0f5272a47fad61194cfdb9f62a231a8087676aaa13feb5
|
|
4
|
+
data.tar.gz: b5dad730b6e6c6b7244cdd4b8080b4d23927f2c323055b0240c603e3c6230c6f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f746efa34e732f4b0a855d60b61db5c8d13f767d4beeb4d0ec7d0a736daac5164b6c74941a30cba9472437e67734ed2d98d7837eac5a6ef4c7279169cc0b17e7
|
|
7
|
+
data.tar.gz: 587da609dfce7bab98b8333aba463ec784f5da814ec80b9624ed61c622e92c903586abeb33e3af3f33c2d387753a755cecaa82d3d9620b378b3533a337b900ad
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.4] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Carve.to_html_with_includes` renders a file-backed document with its
|
|
15
|
+
`{{ path }}` directives expanded, contained to an absolute root the caller
|
|
16
|
+
names. It returns the rendered HTML alongside sanitized warnings and
|
|
17
|
+
root-relative dependencies, and forwards extension and include budget
|
|
18
|
+
settings to the engine. The binding canonicalizes the root and the source
|
|
19
|
+
path and requires the source to sit inside the root (#123, #125).
|
|
20
|
+
- `Carve.parse_with_includes` publishes the expanded document as a tree, in the
|
|
21
|
+
shape `Carve.parse` returns, and the include path takes the same render
|
|
22
|
+
options the string entry points take. A consumer that draws from the tree
|
|
23
|
+
rather than from HTML, such as carve-hexapdf, was blocked without it. Its
|
|
24
|
+
nodes carry no `pos`: spec I4 leaves position remapping across an include out
|
|
25
|
+
of scope in every engine (#126, #127).
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- HTML and Markdown migration reports use schema version 2 with shared
|
|
30
|
+
preserved, normalized, degraded, and dropped fidelity plus confidence.
|
|
31
|
+
Markdown retains the existing `source_format` key as a compatibility alias
|
|
32
|
+
for the shared `sourceFormat` spelling and now emits a conservative
|
|
33
|
+
`fidelity-unverified` dropped/fallback finding on every import instead of
|
|
34
|
+
the previous empty diagnostics array (#117).
|
|
35
|
+
- **Breaking:** A substitution node in the tree carries `old` and `new` as
|
|
36
|
+
arrays of inline nodes, where it carried the strings `oldText` and `newText`.
|
|
37
|
+
A caller reading that node walks the halves instead of reading them (#130,
|
|
38
|
+
markup-carve/carve-rs#1756).
|
|
39
|
+
- The engine moves from carve-rs `42df4092` to released 0.1.6 (`d7837249`).
|
|
40
|
+
Beyond the substitution change, the range brings the include pass this
|
|
41
|
+
release exposes, mention and tag handling (a nameless mention or tag is
|
|
42
|
+
dropped and reported, an unspellable name is refused, attributes survive the
|
|
43
|
+
editor bridge), and writer and parser fixes: the Markdown and Carve writers
|
|
44
|
+
escape what would reopen a construct on the way back in, and parsing tightens
|
|
45
|
+
around braced inlines, forced closers, escaped markers, adjacent links, blank
|
|
46
|
+
table rows and a code span's closer. 1740 of 1740 corpus documents render
|
|
47
|
+
byte-identically, and `resources/spec-drift.txt` is empty.
|
|
48
|
+
|
|
10
49
|
## [0.1.3] - 2026-09-08
|
|
11
50
|
|
|
12
51
|
### Changed
|
|
@@ -324,7 +363,8 @@ are not listed, because no release ever shipped them.
|
|
|
324
363
|
Arrays (every AST node type is covered), enabling custom renderers such as
|
|
325
364
|
[carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
|
|
326
365
|
|
|
327
|
-
[Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.
|
|
366
|
+
[Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.4...HEAD
|
|
367
|
+
[0.1.4]: https://github.com/markup-carve/carve-rb/compare/v0.1.3...v0.1.4
|
|
328
368
|
[0.1.3]: https://github.com/markup-carve/carve-rb/compare/v0.1.2...v0.1.3
|
|
329
369
|
[0.1.2]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...v0.1.2
|
|
330
370
|
[0.1.1]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
|
@@ -37,15 +37,18 @@ time via `rb_sys`.
|
|
|
37
37
|
require "carve"
|
|
38
38
|
|
|
39
39
|
Carve.to_html("# Hello *world*")
|
|
40
|
+
# => "<section id=\"Hello-world\">\n <h1>Hello <strong>world</strong></h1>\n</section>"
|
|
40
41
|
|
|
41
42
|
# Every core engine target is available from the binding.
|
|
42
43
|
Carve.to_markdown(source)
|
|
43
44
|
Carve.to_plain_text(source)
|
|
44
45
|
Carve.to_ansi(source)
|
|
45
46
|
Carve.to_carve(source)
|
|
47
|
+
# Import reports use schema version 2, with fidelity and confidence per finding.
|
|
48
|
+
# Markdown emits fidelity-unverified/dropped/fallback until its engine path
|
|
49
|
+
# exposes construct-level fidelity.
|
|
46
50
|
Carve.from_html('<p>Hello <strong>world</strong></p>')
|
|
47
51
|
Carve.from_markdown('*em* and **strong**')
|
|
48
|
-
# => "<section id=\"Hello-world\">\n <h1>Hello <strong>world</strong></h1>\n</section>"
|
|
49
52
|
|
|
50
53
|
# Carve syntax note: *...* is STRONG (bold), /.../ is EMPHASIS (italic).
|
|
51
54
|
Carve.to_html("*bold* and /italic/")
|
|
@@ -77,6 +80,57 @@ this binding has always taken: `:math`, `:permalinks`, `:mermaid`, `:dot`,
|
|
|
77
80
|
|
|
78
81
|
An unknown extension name raises `ArgumentError`.
|
|
79
82
|
|
|
83
|
+
### File includes
|
|
84
|
+
|
|
85
|
+
Includes are opt-in and require an absolute containment root plus the absolute
|
|
86
|
+
path of the source document. The result carries the rendered value, warnings,
|
|
87
|
+
and root-relative dependencies. Other render methods leave include directives
|
|
88
|
+
literal.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
result = Carve.to_html_with_includes(
|
|
92
|
+
File.read("book.crv"),
|
|
93
|
+
root: File.expand_path("."),
|
|
94
|
+
source_path: File.expand_path("book.crv"),
|
|
95
|
+
max_depth: 16,
|
|
96
|
+
)
|
|
97
|
+
puts result[:value]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Every render option `Carve.to_html` takes is accepted here and reaches the
|
|
101
|
+
included children too, so a document renders the same way whether or not it
|
|
102
|
+
went through the include path: `extensions:`, `symbols:`, `profile:`, `mode:`,
|
|
103
|
+
`renderers:`, `safe:` and `sections:`.
|
|
104
|
+
|
|
105
|
+
`Carve.parse_with_includes` gives the expanded document as an AST instead, in
|
|
106
|
+
the shape `Carve.parse` returns, for a host that walks the tree rather than
|
|
107
|
+
rendering HTML:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
result = Carve.parse_with_includes(
|
|
111
|
+
File.read("book.crv"),
|
|
112
|
+
root: File.expand_path("."),
|
|
113
|
+
source_path: File.expand_path("book.crv"),
|
|
114
|
+
)
|
|
115
|
+
result[:value][:children]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Its nodes carry no `:pos`, unlike `Carve.parse`. Spec I4 leaves position
|
|
119
|
+
remapping out of scope in every engine, so a span on an included node would name
|
|
120
|
+
an offset in a document the caller never passed.
|
|
121
|
+
|
|
122
|
+
`Carve.render_with_includes` takes the same arguments plus `target:`, one of
|
|
123
|
+
`"html"`, `"markdown"`, `"plain"`, `"ansi"` or `"ast"`. There is no `"carve"`
|
|
124
|
+
target: spec I15 excludes the Carve writer from expansion, because inlining a
|
|
125
|
+
child into the formatter's output rewrites the author's document rather than
|
|
126
|
+
formatting it.
|
|
127
|
+
|
|
128
|
+
The budgets `max_depth:`, `max_bytes:`, `max_resolver_calls:` and
|
|
129
|
+
`max_warnings:` pass through to the engine; omit one to keep its default. A
|
|
130
|
+
target refused by the byte budget still reports `resolved: true`, because
|
|
131
|
+
section 19 charges the budget for what the resolver handed back and a target is
|
|
132
|
+
resolved before its size is known.
|
|
133
|
+
|
|
80
134
|
## Parsing to an AST
|
|
81
135
|
|
|
82
136
|
`Carve.parse` returns the parsed document as a tree of Ruby Hashes and Arrays,
|
|
@@ -267,6 +321,9 @@ a document change.
|
|
|
267
321
|
| `Carve.to_html(source, symbols: {...})` | Render with a `:name:` -> value symbol map (values are raw, see above). |
|
|
268
322
|
| `Carve.to_html(source, safe: true, profile: :comment)` | Render untrusted input: escape `=html` raw blocks/spans, restrict constructs. |
|
|
269
323
|
| `Carve.to_html(source, sections: false)` | Render headings flat, with the id on the `<h*>` instead of a `<section>` wrapper. |
|
|
324
|
+
| `Carve.to_html_with_includes(source, root:, source_path:)` | Render contained file includes and return warnings and dependencies. |
|
|
325
|
+
| `Carve.parse_with_includes(source, root:, source_path:)` | The same expansion, published as an AST instead of HTML. |
|
|
326
|
+
| `Carve.render_with_includes(source, root:, source_path:, target:)` | The expansion over any render target: html, markdown, plain, ansi or ast. |
|
|
270
327
|
| `Carve.read_stamp(source)` | Read a document's provenance marker: `{version:, generated_by:}` or `nil`. |
|
|
271
328
|
| `Carve.needs_review?(source)` | Whether a document predates this engine's spec version (unstamped counts as yes). |
|
|
272
329
|
| `Carve.to_html_with_extensions(source, names_array)` | Native primitive (Array of Strings). |
|
data/ext/carve/Cargo.lock
CHANGED
|
@@ -37,8 +37,8 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "carve-lang"
|
|
40
|
-
version = "0.1.
|
|
41
|
-
source = "git+https://github.com/markup-carve/carve-rs?rev=
|
|
40
|
+
version = "0.1.6"
|
|
41
|
+
source = "git+https://github.com/markup-carve/carve-rs?rev=d7837249c64b88879b04cd71b8b5555ff246e16f#d7837249c64b88879b04cd71b8b5555ff246e16f"
|
|
42
42
|
dependencies = [
|
|
43
43
|
"html5ever",
|
|
44
44
|
"markup5ever_rcdom",
|
|
@@ -51,7 +51,7 @@ dependencies = [
|
|
|
51
51
|
|
|
52
52
|
[[package]]
|
|
53
53
|
name = "carve-rb"
|
|
54
|
-
version = "0.1.
|
|
54
|
+
version = "0.1.4"
|
|
55
55
|
dependencies = [
|
|
56
56
|
"carve-lang",
|
|
57
57
|
"magnus",
|
data/ext/carve/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "carve-rb"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.75"
|
|
6
6
|
publish = false
|
|
@@ -22,4 +22,4 @@ serde_json = "1"
|
|
|
22
22
|
# The Carve engine. Imported under the Rust crate alias `carve_rs` so the
|
|
23
23
|
# binding's own crate name (`carve-rb`) does not collide with the engine's
|
|
24
24
|
# package name (`carve`) in the Cargo lockfile.
|
|
25
|
-
carve_rs = { package = "carve-lang", git = "https://github.com/markup-carve/carve-rs", rev = "
|
|
25
|
+
carve_rs = { package = "carve-lang", git = "https://github.com/markup-carve/carve-rs", rev = "d7837249c64b88879b04cd71b8b5555ff246e16f" }
|
data/ext/carve/src/lib.rs
CHANGED
|
@@ -28,6 +28,7 @@ use carve_rs::extensions::registry;
|
|
|
28
28
|
use carve_rs::{CarveExtension, Mode, Options, Profile, StaticRenderers};
|
|
29
29
|
use magnus::value::{InnerValue, Opaque};
|
|
30
30
|
use magnus::{function, prelude::*, Error, RArray, RHash, Ruby, Value};
|
|
31
|
+
use std::path::{Path, PathBuf};
|
|
31
32
|
|
|
32
33
|
/// HTML-escape a string for the renderer-failure fallback path.
|
|
33
34
|
///
|
|
@@ -215,6 +216,218 @@ fn to_carve(source: String) -> String {
|
|
|
215
216
|
carve_rs::to_carve(&source)
|
|
216
217
|
}
|
|
217
218
|
|
|
219
|
+
/// Which renderer runs over the expanded document.
|
|
220
|
+
///
|
|
221
|
+
/// `carve` is absent on purpose: spec I15 excludes the Carve writer from
|
|
222
|
+
/// expansion, because inlining a child into the formatter's output rewrites the
|
|
223
|
+
/// author's document rather than formatting it.
|
|
224
|
+
#[derive(Clone, Copy, PartialEq)]
|
|
225
|
+
enum IncludeTarget {
|
|
226
|
+
Html,
|
|
227
|
+
Markdown,
|
|
228
|
+
Plain,
|
|
229
|
+
Ansi,
|
|
230
|
+
Ast,
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn parse_include_target(ruby: &Ruby, name: &str) -> Result<IncludeTarget, Error> {
|
|
234
|
+
match name {
|
|
235
|
+
"html" => Ok(IncludeTarget::Html),
|
|
236
|
+
"markdown" => Ok(IncludeTarget::Markdown),
|
|
237
|
+
"plain" => Ok(IncludeTarget::Plain),
|
|
238
|
+
"ansi" => Ok(IncludeTarget::Ansi),
|
|
239
|
+
"ast" => Ok(IncludeTarget::Ast),
|
|
240
|
+
other => Err(Error::new(
|
|
241
|
+
ruby.exception_arg_error(),
|
|
242
|
+
format!(
|
|
243
|
+
"Unknown Carve include target: {other:?} \
|
|
244
|
+
(supported: \"html\", \"markdown\", \"plain\", \"ansi\", \"ast\")"
|
|
245
|
+
),
|
|
246
|
+
)),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#[allow(clippy::too_many_arguments)]
|
|
251
|
+
fn render_with_includes_json(
|
|
252
|
+
ruby: &Ruby,
|
|
253
|
+
source: String,
|
|
254
|
+
root: String,
|
|
255
|
+
source_path: String,
|
|
256
|
+
target: String,
|
|
257
|
+
names: RArray,
|
|
258
|
+
mode: String,
|
|
259
|
+
renderers: RHash,
|
|
260
|
+
symbols: RHash,
|
|
261
|
+
safe: bool,
|
|
262
|
+
profile: Option<String>,
|
|
263
|
+
sections: bool,
|
|
264
|
+
max_depth: Option<usize>,
|
|
265
|
+
max_bytes: Option<usize>,
|
|
266
|
+
max_resolver_calls: Option<usize>,
|
|
267
|
+
max_warnings: Option<usize>,
|
|
268
|
+
) -> Result<String, Error> {
|
|
269
|
+
let which = parse_include_target(ruby, &target)?;
|
|
270
|
+
let root_path = Path::new(&root);
|
|
271
|
+
let source_path = Path::new(&source_path);
|
|
272
|
+
if !root_path.is_absolute() || !source_path.is_absolute() {
|
|
273
|
+
return Err(Error::new(
|
|
274
|
+
ruby.exception_arg_error(),
|
|
275
|
+
"include root and source_path must be absolute".to_string(),
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
let root_real = std::fs::canonicalize(root_path).map_err(|_| {
|
|
279
|
+
Error::new(
|
|
280
|
+
ruby.exception_arg_error(),
|
|
281
|
+
"include root is not a readable directory".to_string(),
|
|
282
|
+
)
|
|
283
|
+
})?;
|
|
284
|
+
let source_real = std::fs::canonicalize(source_path).map_err(|_| {
|
|
285
|
+
Error::new(
|
|
286
|
+
ruby.exception_arg_error(),
|
|
287
|
+
"include source_path is not a readable file".to_string(),
|
|
288
|
+
)
|
|
289
|
+
})?;
|
|
290
|
+
if !source_real.starts_with(&root_real) || !source_real.is_file() {
|
|
291
|
+
return Err(Error::new(
|
|
292
|
+
ruby.exception_arg_error(),
|
|
293
|
+
"include source_path must be a file inside the include root".to_string(),
|
|
294
|
+
));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let resolver = carve_rs::FileSystemResolver::new(&root_real).map_err(|_| {
|
|
298
|
+
Error::new(
|
|
299
|
+
ruby.exception_arg_error(),
|
|
300
|
+
"include root is not usable".to_string(),
|
|
301
|
+
)
|
|
302
|
+
})?;
|
|
303
|
+
// The same options `to_html` builds. A document rendered through the
|
|
304
|
+
// include path renders the way it would without one: jekyll-carve passes a
|
|
305
|
+
// symbol map on every conversion, and carve-hexapdf asks for one profile
|
|
306
|
+
// and extension set across parent and child alike.
|
|
307
|
+
let parsed_mode = parse_mode(ruby, &mode)?;
|
|
308
|
+
let parsed_profile = match profile.as_deref() {
|
|
309
|
+
None => None,
|
|
310
|
+
Some(name) => Some(parse_profile(ruby, name)?),
|
|
311
|
+
};
|
|
312
|
+
let static_renderers = build_renderers(ruby, renderers)?;
|
|
313
|
+
let boxed = boxed_extensions(ruby, names)?;
|
|
314
|
+
let symbol_pairs = build_symbols(symbols)?;
|
|
315
|
+
let mut render_options = Options::new()
|
|
316
|
+
.with_mode(parsed_mode)
|
|
317
|
+
.with_renderers(static_renderers);
|
|
318
|
+
for extension in &boxed {
|
|
319
|
+
render_options = render_options.with_extension(extension.as_ref());
|
|
320
|
+
}
|
|
321
|
+
for (name, value) in &symbol_pairs {
|
|
322
|
+
render_options = render_options.with_symbol(name.clone(), value.clone());
|
|
323
|
+
}
|
|
324
|
+
if safe {
|
|
325
|
+
render_options = render_options.with_raw_html(false);
|
|
326
|
+
}
|
|
327
|
+
if let Some(preset) = parsed_profile {
|
|
328
|
+
render_options = render_options.with_profile(preset);
|
|
329
|
+
}
|
|
330
|
+
if !sections {
|
|
331
|
+
render_options = render_options.with_sections(false);
|
|
332
|
+
}
|
|
333
|
+
let mut include_options = carve_rs::IncludeOptions::new()
|
|
334
|
+
.with_resolver(&resolver)
|
|
335
|
+
.with_source_path(source_real.to_string_lossy());
|
|
336
|
+
if let Some(value) = max_depth {
|
|
337
|
+
include_options = include_options.with_max_depth(value);
|
|
338
|
+
}
|
|
339
|
+
if let Some(value) = max_bytes {
|
|
340
|
+
include_options = include_options.with_max_bytes(value);
|
|
341
|
+
}
|
|
342
|
+
if let Some(value) = max_resolver_calls {
|
|
343
|
+
include_options = include_options.with_max_resolver_calls(value);
|
|
344
|
+
}
|
|
345
|
+
if let Some(value) = max_warnings {
|
|
346
|
+
include_options = include_options.with_max_warnings(value);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let target_is_html = which == IncludeTarget::Html;
|
|
350
|
+
let prepared = carve_rs::prepare_doc_with_includes(
|
|
351
|
+
&source,
|
|
352
|
+
&render_options,
|
|
353
|
+
&include_options,
|
|
354
|
+
// Every target but HTML is inherently static, so it prepares under the
|
|
355
|
+
// interactive mode the other renderers assume.
|
|
356
|
+
if target_is_html {
|
|
357
|
+
render_options.mode
|
|
358
|
+
} else {
|
|
359
|
+
Mode::Interactive
|
|
360
|
+
},
|
|
361
|
+
target_is_html,
|
|
362
|
+
)
|
|
363
|
+
.map_err(|error| Error::new(ruby.exception_arg_error(), error.to_string()))?;
|
|
364
|
+
let rendered = match which {
|
|
365
|
+
IncludeTarget::Html => carve_rs::render_html_with_options(&prepared.doc, &render_options),
|
|
366
|
+
IncludeTarget::Markdown => {
|
|
367
|
+
carve_rs::render_markdown_with_options(&prepared.doc, &render_options)
|
|
368
|
+
}
|
|
369
|
+
IncludeTarget::Plain => {
|
|
370
|
+
carve_rs::render_plain_text_with_options(&prepared.doc, &render_options)
|
|
371
|
+
}
|
|
372
|
+
IncludeTarget::Ansi => carve_rs::render_ansi_with_options(&prepared.doc, &render_options),
|
|
373
|
+
// The AST is published as a tree, not as a string, so a caller does not
|
|
374
|
+
// parse JSON twice. Positions stay OFF, unlike `to_ast_json`: spec I4
|
|
375
|
+
// leaves position remapping out of scope in every engine, so a child's
|
|
376
|
+
// spans would point into a document the caller never passed.
|
|
377
|
+
IncludeTarget::Ast => Ok(carve_rs::to_json(&prepared.doc)),
|
|
378
|
+
}
|
|
379
|
+
.map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
|
|
380
|
+
let value: serde_json::Value = if which == IncludeTarget::Ast {
|
|
381
|
+
serde_json::from_str(&rendered).map_err(|error| {
|
|
382
|
+
Error::new(
|
|
383
|
+
ruby.exception_runtime_error(),
|
|
384
|
+
format!("the engine's AST JSON did not parse: {error}"),
|
|
385
|
+
)
|
|
386
|
+
})?
|
|
387
|
+
} else {
|
|
388
|
+
serde_json::Value::String(rendered)
|
|
389
|
+
};
|
|
390
|
+
let safe_path = |value: &str| {
|
|
391
|
+
let path = PathBuf::from(value);
|
|
392
|
+
if path.is_absolute() {
|
|
393
|
+
path.strip_prefix(&root_real)
|
|
394
|
+
.map(|relative| relative.to_string_lossy().replace('\\', "/"))
|
|
395
|
+
.unwrap_or_else(|_| "[outside-root]".to_string())
|
|
396
|
+
} else {
|
|
397
|
+
value.to_string()
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
let warnings = prepared
|
|
401
|
+
.warnings
|
|
402
|
+
.iter()
|
|
403
|
+
.map(|warning| {
|
|
404
|
+
serde_json::json!({
|
|
405
|
+
"rule": warning.rule,
|
|
406
|
+
"message": warning.message,
|
|
407
|
+
"file": warning.file.as_deref().map(&safe_path),
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
.collect::<Vec<_>>();
|
|
411
|
+
let dependencies = prepared
|
|
412
|
+
.dependencies
|
|
413
|
+
.iter()
|
|
414
|
+
.map(|dependency| {
|
|
415
|
+
serde_json::json!({
|
|
416
|
+
"path": safe_path(&dependency.id),
|
|
417
|
+
"resolved": dependency.resolved,
|
|
418
|
+
"denial": dependency.denial.map(|denial| denial.as_str()),
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
.collect::<Vec<_>>();
|
|
422
|
+
Ok(serde_json::json!({
|
|
423
|
+
"value": value,
|
|
424
|
+
"warnings": warnings,
|
|
425
|
+
"dependencies": dependencies,
|
|
426
|
+
"suppressedWarnings": prepared.suppressed_warnings,
|
|
427
|
+
})
|
|
428
|
+
.to_string())
|
|
429
|
+
}
|
|
430
|
+
|
|
218
431
|
fn from_html_json(ruby: &Ruby, source: String, mode: String) -> Result<String, Error> {
|
|
219
432
|
let mode = carve_rs::HtmlImportMode::from_name(&mode).ok_or_else(|| {
|
|
220
433
|
Error::new(
|
|
@@ -222,7 +435,7 @@ fn from_html_json(ruby: &Ruby, source: String, mode: String) -> Result<String, E
|
|
|
222
435
|
"mode must be safe, semantic, or roundtrip".to_string(),
|
|
223
436
|
)
|
|
224
437
|
})?;
|
|
225
|
-
let result = carve_rs::
|
|
438
|
+
let result = carve_rs::migrate_html(
|
|
226
439
|
&source,
|
|
227
440
|
&carve_rs::HtmlImportOptions {
|
|
228
441
|
mode,
|
|
@@ -239,31 +452,44 @@ fn from_html_json(ruby: &Ruby, source: String, mode: String) -> Result<String, E
|
|
|
239
452
|
.report
|
|
240
453
|
.diagnostics
|
|
241
454
|
.iter()
|
|
242
|
-
.map(
|
|
243
|
-
let mut value = serde_json::json!({
|
|
244
|
-
"code": diagnostic.code.as_str(),
|
|
245
|
-
"message": diagnostic.message,
|
|
246
|
-
"severity": diagnostic.severity.as_str(),
|
|
247
|
-
});
|
|
248
|
-
if let Some(path) = &diagnostic.path {
|
|
249
|
-
value["path"] = serde_json::json!(path);
|
|
250
|
-
}
|
|
251
|
-
value
|
|
252
|
-
})
|
|
455
|
+
.map(migration_diagnostic_json)
|
|
253
456
|
.collect::<Vec<_>>();
|
|
254
457
|
Ok(serde_json::json!({
|
|
255
458
|
"value": result.value,
|
|
256
459
|
"report": {
|
|
257
|
-
"
|
|
258
|
-
"
|
|
460
|
+
"schemaVersion": result.report.schema_version,
|
|
461
|
+
"sourceFormat": result.report.source_format.as_str(),
|
|
462
|
+
"mode": result.report.mode.map(|value| value.as_str()),
|
|
463
|
+
"adapter": result.report.adapter.map(|value| value.as_str()),
|
|
259
464
|
"diagnostics": diagnostics,
|
|
260
465
|
}
|
|
261
466
|
})
|
|
262
467
|
.to_string())
|
|
263
468
|
}
|
|
264
469
|
|
|
265
|
-
fn
|
|
266
|
-
carve_rs::
|
|
470
|
+
fn from_markdown_json(source: String) -> String {
|
|
471
|
+
let result = carve_rs::migrate_markdown(&source);
|
|
472
|
+
let diagnostics = result
|
|
473
|
+
.report
|
|
474
|
+
.diagnostics
|
|
475
|
+
.iter()
|
|
476
|
+
.map(migration_diagnostic_json)
|
|
477
|
+
.collect::<Vec<_>>();
|
|
478
|
+
serde_json::json!({"value": result.value, "report": {"schemaVersion": result.report.schema_version, "sourceFormat": result.report.source_format.as_str(), "diagnostics": diagnostics}}).to_string()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn migration_diagnostic_json(diagnostic: &carve_rs::MigrationDiagnostic) -> serde_json::Value {
|
|
482
|
+
let mut value = serde_json::json!({
|
|
483
|
+
"code": diagnostic.code,
|
|
484
|
+
"message": diagnostic.message,
|
|
485
|
+
"severity": diagnostic.severity.as_str(),
|
|
486
|
+
"fidelity": diagnostic.fidelity.as_str(),
|
|
487
|
+
"confidence": diagnostic.confidence.as_str(),
|
|
488
|
+
});
|
|
489
|
+
if let Some(path) = &diagnostic.path {
|
|
490
|
+
value["path"] = serde_json::json!(path);
|
|
491
|
+
}
|
|
492
|
+
value
|
|
267
493
|
}
|
|
268
494
|
|
|
269
495
|
/// Parse Carve source and return its AST as a JSON string.
|
|
@@ -523,8 +749,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
523
749
|
module.define_singleton_method("to_plain_text", function!(to_plain_text, 1))?;
|
|
524
750
|
module.define_singleton_method("to_ansi", function!(to_ansi, 1))?;
|
|
525
751
|
module.define_singleton_method("to_carve", function!(to_carve, 1))?;
|
|
752
|
+
module.define_singleton_method(
|
|
753
|
+
"_render_with_includes_json",
|
|
754
|
+
function!(render_with_includes_json, 15),
|
|
755
|
+
)?;
|
|
526
756
|
module.define_singleton_method("_from_html_json", function!(from_html_json, 2))?;
|
|
527
|
-
module.define_singleton_method("
|
|
757
|
+
module.define_singleton_method("_from_markdown_json", function!(from_markdown_json, 1))?;
|
|
528
758
|
module.define_singleton_method("_to_ast_json", function!(to_ast_json, 1))?;
|
|
529
759
|
module.define_singleton_method(
|
|
530
760
|
"to_html_with_extensions",
|
data/lib/carve/version.rb
CHANGED
|
@@ -8,5 +8,5 @@ module Carve
|
|
|
8
8
|
# compares it against ext/carve/Cargo.toml and against the newest cut
|
|
9
9
|
# CHANGELOG section on every run, and .github/workflows/release.yml refuses to
|
|
10
10
|
# publish a gem whose version is not the tag being released.
|
|
11
|
-
VERSION = "0.1.
|
|
11
|
+
VERSION = "0.1.4"
|
|
12
12
|
end
|
data/lib/carve.rb
CHANGED
|
@@ -42,6 +42,94 @@ module Carve
|
|
|
42
42
|
RENDERER_KEYS = %i[mermaid chart graphviz math].freeze
|
|
43
43
|
|
|
44
44
|
class << self
|
|
45
|
+
# Render +source+ to HTML with its <tt>{{ path }}</tt> includes expanded,
|
|
46
|
+
# contained to +root:+.
|
|
47
|
+
#
|
|
48
|
+
# Carve.to_html_with_includes(File.read(page), root: "/srv/site",
|
|
49
|
+
# source_path: page, symbols: {smile: "\u{1F604}"})
|
|
50
|
+
#
|
|
51
|
+
# +root:+ and +source_path:+ are absolute, and the source file lives inside
|
|
52
|
+
# the root. A relative value raises ArgumentError rather than rooting
|
|
53
|
+
# containment at the process working directory, which is arbitrary with
|
|
54
|
+
# respect to the document.
|
|
55
|
+
#
|
|
56
|
+
# Returns +{value:, warnings:, dependencies:, suppressedWarnings:}+. Paths
|
|
57
|
+
# in +warnings+ and +dependencies+ are relative to the root, so a report can
|
|
58
|
+
# be shown to a reader as it stands.
|
|
59
|
+
#
|
|
60
|
+
# Every render option +to_html+ takes is accepted and reaches the included
|
|
61
|
+
# children too: an extension, a symbol map or a profile means the same thing
|
|
62
|
+
# in a child as in the parent.
|
|
63
|
+
def to_html_with_includes(source, root:, source_path:, extensions: nil, mode: nil,
|
|
64
|
+
renderers: nil, symbols: nil, safe: false, profile: nil,
|
|
65
|
+
sections: true, max_depth: nil, max_bytes: nil,
|
|
66
|
+
max_resolver_calls: nil, max_warnings: nil)
|
|
67
|
+
render_with_includes(
|
|
68
|
+
source, target: "html", root: root, source_path: source_path,
|
|
69
|
+
extensions: extensions, mode: mode, renderers: renderers, symbols: symbols,
|
|
70
|
+
safe: safe, profile: profile, sections: sections, max_depth: max_depth,
|
|
71
|
+
max_bytes: max_bytes, max_resolver_calls: max_resolver_calls,
|
|
72
|
+
max_warnings: max_warnings
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The expanded document as an AST, in the shape +.parse+ returns.
|
|
77
|
+
#
|
|
78
|
+
# Carve.parse_with_includes(File.read(page), root: root, source_path: page)
|
|
79
|
+
# # => {value: {type: "document", ...}, warnings: [], dependencies: [...], ...}
|
|
80
|
+
#
|
|
81
|
+
# For a host that walks the tree rather than rendering HTML - carve-hexapdf
|
|
82
|
+
# draws its PDF from this shape.
|
|
83
|
+
#
|
|
84
|
+
# Nodes carry NO +:pos+, unlike +.parse+. Spec I4 leaves position remapping
|
|
85
|
+
# out of scope in every engine, so a span on an included node would name an
|
|
86
|
+
# offset in a document the caller never passed.
|
|
87
|
+
def parse_with_includes(source, root:, source_path:, extensions: nil, profile: nil,
|
|
88
|
+
max_depth: nil, max_bytes: nil, max_resolver_calls: nil,
|
|
89
|
+
max_warnings: nil)
|
|
90
|
+
render_with_includes(
|
|
91
|
+
source, target: "ast", root: root, source_path: source_path,
|
|
92
|
+
extensions: extensions, profile: profile, max_depth: max_depth,
|
|
93
|
+
max_bytes: max_bytes, max_resolver_calls: max_resolver_calls,
|
|
94
|
+
max_warnings: max_warnings
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The include pass over any render target: +"html"+, +"markdown"+,
|
|
99
|
+
# +"plain"+, +"ansi"+ or +"ast"+.
|
|
100
|
+
#
|
|
101
|
+
# There is no +"carve"+ target. Spec I15 excludes the Carve writer from
|
|
102
|
+
# expansion, because inlining a child into the formatter's output rewrites
|
|
103
|
+
# the author's document rather than formatting it.
|
|
104
|
+
def render_with_includes(source, root:, source_path:, target: "html", extensions: nil,
|
|
105
|
+
mode: nil, renderers: nil, symbols: nil, safe: false,
|
|
106
|
+
profile: nil, sections: true, max_depth: nil, max_bytes: nil,
|
|
107
|
+
max_resolver_calls: nil, max_warnings: nil)
|
|
108
|
+
JSON.parse(
|
|
109
|
+
_render_with_includes_json(
|
|
110
|
+
source.to_s,
|
|
111
|
+
root.to_s,
|
|
112
|
+
source_path.to_s,
|
|
113
|
+
target.to_s,
|
|
114
|
+
Array(extensions).map(&:to_s),
|
|
115
|
+
(mode || :interactive).to_s,
|
|
116
|
+
renderers || {},
|
|
117
|
+
symbols || {},
|
|
118
|
+
!!safe,
|
|
119
|
+
profile&.to_s,
|
|
120
|
+
!!sections,
|
|
121
|
+
max_depth,
|
|
122
|
+
max_bytes,
|
|
123
|
+
max_resolver_calls,
|
|
124
|
+
max_warnings,
|
|
125
|
+
),
|
|
126
|
+
symbolize_names: true,
|
|
127
|
+
# The engine bounds nesting itself, above Ruby JSON's default of 100, so
|
|
128
|
+
# a legal document would otherwise raise here. Same reason as .parse.
|
|
129
|
+
max_nesting: false,
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
45
133
|
# Import HTML or Markdown into canonical Carve. Both methods return the
|
|
46
134
|
# shared migration shape `{ value:, report: }`.
|
|
47
135
|
def from_html(source, mode: :safe)
|
|
@@ -49,10 +137,10 @@ module Carve
|
|
|
49
137
|
end
|
|
50
138
|
|
|
51
139
|
def from_markdown(source)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
140
|
+
result = JSON.parse(_from_markdown_json(source), symbolize_names: true)
|
|
141
|
+
# Keep the 0.1.x spelling while exposing the shared cross-language key.
|
|
142
|
+
result[:report][:source_format] = result[:report][:sourceFormat]
|
|
143
|
+
result
|
|
56
144
|
end
|
|
57
145
|
|
|
58
146
|
# Render Carve +source+ to an HTML string.
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carve-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- markup-carve
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|