carve-lang 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9dc82ae49fa620a500b09b60ad705caca3488b99c18ec3dda8ff22f1aabc5b55
4
- data.tar.gz: 192a18307bfb8cc9bd9364a333592b92bf0cd9075f2403fa602b09307705912b
3
+ metadata.gz: 8f7b8390c9d7d5e50b0f5272a47fad61194cfdb9f62a231a8087676aaa13feb5
4
+ data.tar.gz: b5dad730b6e6c6b7244cdd4b8080b4d23927f2c323055b0240c603e3c6230c6f
5
5
  SHA512:
6
- metadata.gz: a0eb63cc0fb830f1d6a69136d7a934931069718a700aa280017313e05a704614ef805bd9a136ce17620fd88188bb39cd4e335e059ecb002683a7a7bf4a646c1c
7
- data.tar.gz: 33039444b491c79db93ef2041556d4ec40533ec98ed1dec8cc154c79b4b147ffde9a8533bf8df0839ac72c74da352d5087251c1eccb5521d769d0ff3a202a035
6
+ metadata.gz: f746efa34e732f4b0a855d60b61db5c8d13f767d4beeb4d0ec7d0a736daac5164b6c74941a30cba9472437e67734ed2d98d7837eac5a6ef4c7279169cc0b17e7
7
+ data.tar.gz: 587da609dfce7bab98b8333aba463ec784f5da814ec80b9624ed61c622e92c903586abeb33e3af3f33c2d387753a755cecaa82d3d9620b378b3533a337b900ad
data/CHANGELOG.md CHANGED
@@ -7,6 +7,63 @@ 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
+
49
+ ## [0.1.3] - 2026-09-08
50
+
51
+ ### Changed
52
+
53
+ - The engine moves from carve-rs `da45f9d2` to `42df4092`, 46 commits, clearing
54
+ the binding-parity drift the nightly run found on 34 corpus documents (#110).
55
+ A comment leaf span begins at its markup, an emptied definition description
56
+ narrows to its marker line, a footnote nested past a footnote body becomes its
57
+ own note, a nested item's lead fence or colon container takes its flush-left
58
+ body, and an all-blank or lone-pipe row is no longer read as a table (the lone
59
+ pipe no longer panics).
60
+ - The engine moves from carve-rs `2e9c43f2` to `da45f9d2`, 20 commits. A colon
61
+ followed by a space and a tab opens no description, an authored task state
62
+ reaches the tree and the HTML, a titled medium emits one title attribute, an
63
+ empty external link target is omitted, and a table continuation survives Djot
64
+ migration. The stale pin is what failed PART 12 binding parity in
65
+ markup-carve/carve#1877 - a binding has no vote of its own.
66
+
10
67
  ## [0.1.2] - 2026-08-27
11
68
 
12
69
  ### Security
@@ -306,6 +363,9 @@ are not listed, because no release ever shipped them.
306
363
  Arrays (every AST node type is covered), enabling custom renderers such as
307
364
  [carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
308
365
 
309
- [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...HEAD
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
368
+ [0.1.3]: https://github.com/markup-carve/carve-rb/compare/v0.1.2...v0.1.3
369
+ [0.1.2]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...v0.1.2
310
370
  [0.1.1]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...v0.1.1
311
371
  [0.1.0]: https://github.com/markup-carve/carve-rb/releases/tag/v0.1.0
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,18 +37,21 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
37
37
 
38
38
  [[package]]
39
39
  name = "carve-lang"
40
- version = "0.1.4"
41
- source = "git+https://github.com/markup-carve/carve-rs?rev=2e9c43f22c5ed05dbd93323ae6b9c94bb867b285#2e9c43f22c5ed05dbd93323ae6b9c94bb867b285"
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",
45
45
  "pulldown-cmark",
46
46
  "regex",
47
+ "serde",
48
+ "serde_json",
49
+ "unicode-normalization",
47
50
  ]
48
51
 
49
52
  [[package]]
50
53
  name = "carve-rb"
51
- version = "0.1.2"
54
+ version = "0.1.4"
52
55
  dependencies = [
53
56
  "carve-lang",
54
57
  "magnus",
@@ -89,14 +92,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
89
92
  checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
90
93
 
91
94
  [[package]]
92
- name = "futf"
93
- version = "0.1.5"
95
+ name = "fastrand"
96
+ version = "2.5.0"
94
97
  source = "registry+https://github.com/rust-lang/crates.io-index"
95
- checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
96
- dependencies = [
97
- "mac",
98
- "new_debug_unreachable",
99
- ]
98
+ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
100
99
 
101
100
  [[package]]
102
101
  name = "glob"
@@ -106,16 +105,12 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
106
105
 
107
106
  [[package]]
108
107
  name = "html5ever"
109
- version = "0.27.0"
108
+ version = "0.39.0"
110
109
  source = "registry+https://github.com/rust-lang/crates.io-index"
111
- checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
110
+ checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
112
111
  dependencies = [
113
112
  "log",
114
- "mac",
115
113
  "markup5ever",
116
- "proc-macro2",
117
- "quote",
118
- "syn 2.0.118",
119
114
  ]
120
115
 
121
116
  [[package]]
@@ -170,12 +165,6 @@ version = "0.4.33"
170
165
  source = "registry+https://github.com/rust-lang/crates.io-index"
171
166
  checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
172
167
 
173
- [[package]]
174
- name = "mac"
175
- version = "0.1.1"
176
- source = "registry+https://github.com/rust-lang/crates.io-index"
177
- checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
178
-
179
168
  [[package]]
180
169
  name = "magnus"
181
170
  version = "0.8.2"
@@ -201,23 +190,20 @@ dependencies = [
201
190
 
202
191
  [[package]]
203
192
  name = "markup5ever"
204
- version = "0.12.1"
193
+ version = "0.39.0"
205
194
  source = "registry+https://github.com/rust-lang/crates.io-index"
206
- checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
195
+ checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
207
196
  dependencies = [
208
197
  "log",
209
- "phf",
210
- "phf_codegen",
211
- "string_cache",
212
- "string_cache_codegen",
213
198
  "tendril",
199
+ "web_atoms",
214
200
  ]
215
201
 
216
202
  [[package]]
217
203
  name = "markup5ever_rcdom"
218
- version = "0.3.0"
204
+ version = "0.39.0+unofficial"
219
205
  source = "registry+https://github.com/rust-lang/crates.io-index"
220
- checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
206
+ checksum = "3ac010f19d6c4af81eeb4018a39d7a115de9d285af45c126a4ac02e6fc5716b7"
221
207
  dependencies = [
222
208
  "html5ever",
223
209
  "markup5ever",
@@ -278,18 +264,19 @@ dependencies = [
278
264
 
279
265
  [[package]]
280
266
  name = "phf"
281
- version = "0.11.3"
267
+ version = "0.13.1"
282
268
  source = "registry+https://github.com/rust-lang/crates.io-index"
283
- checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
269
+ checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
284
270
  dependencies = [
285
271
  "phf_shared",
272
+ "serde",
286
273
  ]
287
274
 
288
275
  [[package]]
289
276
  name = "phf_codegen"
290
- version = "0.11.3"
277
+ version = "0.13.1"
291
278
  source = "registry+https://github.com/rust-lang/crates.io-index"
292
- checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
279
+ checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
293
280
  dependencies = [
294
281
  "phf_generator",
295
282
  "phf_shared",
@@ -297,19 +284,19 @@ dependencies = [
297
284
 
298
285
  [[package]]
299
286
  name = "phf_generator"
300
- version = "0.11.3"
287
+ version = "0.13.1"
301
288
  source = "registry+https://github.com/rust-lang/crates.io-index"
302
- checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
289
+ checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
303
290
  dependencies = [
291
+ "fastrand",
304
292
  "phf_shared",
305
- "rand",
306
293
  ]
307
294
 
308
295
  [[package]]
309
296
  name = "phf_shared"
310
- version = "0.11.3"
297
+ version = "0.13.1"
311
298
  source = "registry+https://github.com/rust-lang/crates.io-index"
312
- checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
299
+ checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
313
300
  dependencies = [
314
301
  "siphasher",
315
302
  ]
@@ -349,21 +336,6 @@ dependencies = [
349
336
  "proc-macro2",
350
337
  ]
351
338
 
352
- [[package]]
353
- name = "rand"
354
- version = "0.8.7"
355
- source = "registry+https://github.com/rust-lang/crates.io-index"
356
- checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
357
- dependencies = [
358
- "rand_core",
359
- ]
360
-
361
- [[package]]
362
- name = "rand_core"
363
- version = "0.6.4"
364
- source = "registry+https://github.com/rust-lang/crates.io-index"
365
- checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
366
-
367
339
  [[package]]
368
340
  name = "rb-sys"
369
341
  version = "0.9.128"
@@ -457,6 +429,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
457
429
  checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
458
430
  dependencies = [
459
431
  "serde_core",
432
+ "serde_derive",
460
433
  ]
461
434
 
462
435
  [[package]]
@@ -518,9 +491,9 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
518
491
 
519
492
  [[package]]
520
493
  name = "string_cache"
521
- version = "0.8.9"
494
+ version = "0.9.0"
522
495
  source = "registry+https://github.com/rust-lang/crates.io-index"
523
- checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
496
+ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
524
497
  dependencies = [
525
498
  "new_debug_unreachable",
526
499
  "parking_lot",
@@ -531,9 +504,9 @@ dependencies = [
531
504
 
532
505
  [[package]]
533
506
  name = "string_cache_codegen"
534
- version = "0.5.4"
507
+ version = "0.6.1"
535
508
  source = "registry+https://github.com/rust-lang/crates.io-index"
536
- checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
509
+ checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
537
510
  dependencies = [
538
511
  "phf_generator",
539
512
  "phf_shared",
@@ -565,15 +538,28 @@ dependencies = [
565
538
 
566
539
  [[package]]
567
540
  name = "tendril"
568
- version = "0.4.3"
541
+ version = "0.5.1"
569
542
  source = "registry+https://github.com/rust-lang/crates.io-index"
570
- checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
543
+ checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
571
544
  dependencies = [
572
- "futf",
573
- "mac",
574
- "utf-8",
545
+ "new_debug_unreachable",
575
546
  ]
576
547
 
548
+ [[package]]
549
+ name = "tinyvec"
550
+ version = "1.12.0"
551
+ source = "registry+https://github.com/rust-lang/crates.io-index"
552
+ checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
553
+ dependencies = [
554
+ "tinyvec_macros",
555
+ ]
556
+
557
+ [[package]]
558
+ name = "tinyvec_macros"
559
+ version = "0.1.1"
560
+ source = "registry+https://github.com/rust-lang/crates.io-index"
561
+ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
562
+
577
563
  [[package]]
578
564
  name = "unicase"
579
565
  version = "2.9.0"
@@ -587,10 +573,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
587
573
  checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
588
574
 
589
575
  [[package]]
590
- name = "utf-8"
591
- version = "0.7.6"
576
+ name = "unicode-normalization"
577
+ version = "0.1.25"
578
+ source = "registry+https://github.com/rust-lang/crates.io-index"
579
+ checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
580
+ dependencies = [
581
+ "tinyvec",
582
+ ]
583
+
584
+ [[package]]
585
+ name = "web_atoms"
586
+ version = "0.2.6"
592
587
  source = "registry+https://github.com/rust-lang/crates.io-index"
593
- checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
588
+ checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
589
+ dependencies = [
590
+ "phf",
591
+ "phf_codegen",
592
+ "string_cache",
593
+ "string_cache_codegen",
594
+ ]
594
595
 
595
596
  [[package]]
596
597
  name = "windows-link"
@@ -600,12 +601,11 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
600
601
 
601
602
  [[package]]
602
603
  name = "xml5ever"
603
- version = "0.18.1"
604
+ version = "0.39.0"
604
605
  source = "registry+https://github.com/rust-lang/crates.io-index"
605
- checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69"
606
+ checksum = "5ab627f34ff61b80d756180d556f9c68801d836d271b3b8c094504ceca69d221"
606
607
  dependencies = [
607
608
  "log",
608
- "mac",
609
609
  "markup5ever",
610
610
  ]
611
611
 
data/ext/carve/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "carve-rb"
3
- version = "0.1.2"
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 = "2e9c43f22c5ed05dbd93323ae6b9c94bb867b285" }
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::html_to_carve(
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(|diagnostic| {
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
- "mode": result.report.mode.as_str(),
258
- "adapter": result.report.adapter.as_str(),
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 from_markdown(source: String) -> String {
266
- carve_rs::markdown_to_carve(&source)
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("_from_markdown", function!(from_markdown, 1))?;
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.2"
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
- value: _from_markdown(source),
54
- report: { source_format: "markdown", diagnostics: [] }
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.2
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-08-27 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys