carve-lang 0.1.8 → 0.1.10

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: 701231311b5228a33a5ca37b7855209d3521f8ece697824f0b3e3322cb69a30a
4
- data.tar.gz: 869fc6cbce2b49755f7c9e734afd1ec1afc860f508cec75f4e8f3b13b7ed91db
3
+ metadata.gz: 2d4e85e4ab93621b11f1502ca546d64cc0d46e513e4074e6e4014cec8f5aee04
4
+ data.tar.gz: 3100558ccb654d5c2441358ce8a13ba363db7937e0e979be3944780322b62561
5
5
  SHA512:
6
- metadata.gz: c46d50c7fe1dde855c0d4959067b8d99c45a905610c156a68d40ebf1f5b241d8fc036754755309c8a58d54d41e85c0bd3418031eed1fd393f3dfb6e9b5226ee1
7
- data.tar.gz: 23f7732176af5392bd32839ddffc77e565ed0a9d3a68677f9206d19c71db796922301fcc6231f028dc7bf3ca250ccab54912d879ed8b3c2305eff18123cd7e17
6
+ metadata.gz: db44ca3900ce90483edd037944efa79adf6c55a921afe8b00b00a31ba687d6f0f5fce97efba6ec3fb67e69f3c931544bd6b0e09169d62e9a8549b99e0836c693
7
+ data.tar.gz: 28dbc892917d7fb9074005e71cff53fe603bc25ca4d3a54add3042be7813aa4bed7e2db0eea5b950b1a5e2c416de9d9588c38ac7f8985cb90aa126f482d17c86
data/CHANGELOG.md CHANGED
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.10] - 2026-10-23
11
+
12
+ ### Breaking
13
+
14
+ - The engine is the published `carve-lang` 0.1.11 crate. A quote-prefixed `+`
15
+ needs one space after each `>`, so `>+` and `>>+` now render as text, and a
16
+ continuation marker reads only at column 0 or right after a block opener.
17
+ Re-check documents that place `+` elsewhere (#215, markup-carve/carve#3421).
18
+ - Includes of a file that is not valid UTF-8 are refused with an
19
+ `include-non-text` warning instead of being spliced (#215,
20
+ markup-carve/carve-rs#2885).
21
+
22
+ ### Fixes
23
+
24
+ - The carve-lang 0.1.11 pin corrects container ownership for quotes, lazy lines,
25
+ colon divs and footnotes, fences inside nested lists and quotes, and list
26
+ tightness around captions (#215).
27
+ - Includes see through a byte order mark and keep link definitions and region
28
+ markers in place. Markdown import keeps link destinations, autolink brackets
29
+ and pipe escapes as written (#215).
30
+ - `Carve.render_with_includes` with the `markdown`, `plain` and `ansi` targets
31
+ keeps a link definition where it was written, as `Carve.to_markdown` does,
32
+ instead of moving it to the end (#221).
33
+
34
+ ### Improvements
35
+
36
+ - Precompiled gems for `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`,
37
+ `x86_64-darwin`, `arm64-darwin` and `x64-mingw-ucrt` install without a Rust
38
+ toolchain on Ruby 3.0 to 4.0 (#214).
39
+ - `Carve.to_carve(source, align_tables: true)` pads table columns to display
40
+ width (#215).
41
+ - `Carve.select_include_region(source, name, mode:)` resolves a named
42
+ `@region` to a Range of content lines and raises `Carve::IncludeRegionError`
43
+ for a missing or malformed region (#215).
44
+ - An include with `@code` or `@code:lang` splices a file or region as one code
45
+ block (#215).
46
+ - Include warnings carry `includedBy`, the chain of directives that reached the
47
+ file the warning arose in, with root-relative paths, lines and columns (#221).
48
+
49
+ ## [0.1.9] - 2026-10-18
50
+
51
+ ### Breaking
52
+
53
+ - `Carve.parse` and AST include results carry `astVersion: "1.2"` on the root.
54
+ Exact-key readers must accept the new field (#210).
55
+ - The carve-lang 0.1.10 pin adds `resolved` to resolved link and image references
56
+ and `form` to comments. `[text]()` and `![alt]()` render as links and images,
57
+ and a bare `[r]:` defines an empty destination. Escape `\[` to keep that
58
+ spelling literal. Checked render reports can also include
59
+ `table-span-flattened`, `table-header-row-demoted`,
60
+ `table-header-row-hoisted`, and `table-header-cell-demoted` (#207).
61
+
62
+ ### Fixes
63
+
64
+ - Source strings in other encodings such as Shift_JIS are transcoded to UTF-8.
65
+ Malformed UTF-8 and binary bytes become replacement characters instead of
66
+ failing at the native boundary (#207).
67
+
68
+ ### Improvements
69
+
70
+ - The carve-lang 0.1.10 pin adds CSV and TSV table fences and keeps plain-text
71
+ list continuation blocks at their item's indentation. It also corrects
72
+ parsing of nested lists with quotes and containers (#207).
73
+
10
74
  ## [0.1.8] - 2026-10-14
11
75
 
12
76
  ### Breaking
@@ -463,7 +527,9 @@ are not listed, because no release ever shipped them.
463
527
  Arrays (every AST node type is covered), enabling custom renderers such as
464
528
  [carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
465
529
 
466
- [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.8...HEAD
530
+ [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.10...HEAD
531
+ [0.1.10]: https://github.com/markup-carve/carve-rb/compare/v0.1.9...v0.1.10
532
+ [0.1.9]: https://github.com/markup-carve/carve-rb/compare/v0.1.8...v0.1.9
467
533
  [0.1.8]: https://github.com/markup-carve/carve-rb/compare/v0.1.7...v0.1.8
468
534
  [0.1.7]: https://github.com/markup-carve/carve-rb/compare/v0.1.6...v0.1.7
469
535
  [0.1.6]: https://github.com/markup-carve/carve-rb/compare/v0.1.5...v0.1.6
data/README.md CHANGED
@@ -16,7 +16,48 @@ bundle install
16
16
  ```
17
17
 
18
18
  The distribution name is `carve-lang`, while the require path is `carve`.
19
- Building from source requires Rust 1.75 or newer and Ruby development headers.
19
+
20
+ Releases after 0.1.9 ship precompiled gems for `x86_64-linux`,
21
+ `x86_64-linux-musl`, `aarch64-linux`, `x86_64-darwin`, `arm64-darwin` and
22
+ `x64-mingw-ucrt`, with a binary for each Ruby from 3.0 to 4.0 (from 3.1 on
23
+ Windows, where Ruby 3.0 has no UCRT build). On those platforms `gem install`
24
+ needs no Rust toolchain or libclang. Everywhere else, including newer Rubies,
25
+ RubyGems installs the source gem and builds it as described below.
26
+
27
+ ## Installing from source
28
+
29
+ Version 0.1.9 and earlier ship source only, and so does every release on a
30
+ platform or Ruby without a precompiled gem. There `gem install carve-lang`
31
+ compiles a Rust extension on your machine, so the install needs:
32
+
33
+ - A Rust toolchain with `cargo`, Rust 1.77 or newer. [rustup](https://rustup.rs)
34
+ is the usual way to get one.
35
+ - Ruby development headers (`ruby-dev` on Debian and Ubuntu).
36
+ - libclang and clang. The build runs `bindgen` through `rb-sys` to read Ruby's
37
+ C headers. On Debian and Ubuntu:
38
+
39
+ ```bash
40
+ sudo apt install libclang-dev clang
41
+ ```
42
+
43
+ If only libclang is installed, without the `clang` package, the build stops
44
+ with this error:
45
+
46
+ ```text
47
+ /usr/include/ruby-3.2.0/ruby/ruby.h:23:10: fatal error: 'stdarg.h' file not found
48
+ thread 'main' panicked at .../rb-sys-0.9.128/build/main.rs:52:6:
49
+ generate bindings: ClangDiagnostic("/usr/include/ruby-3.2.0/ruby/ruby.h:23:10: fatal error: 'stdarg.h' file not found\n")
50
+ error: failed to run custom build command for `rb-sys v0.9.128`
51
+ ```
52
+
53
+ libclang cannot find its own builtin headers. Install `clang`, or point
54
+ bindgen at GCC's copy of those headers:
55
+
56
+ ```bash
57
+ BINDGEN_EXTRA_CLANG_ARGS="-I$(gcc -print-file-name=include)" gem install carve-lang
58
+ ```
59
+
60
+ With Bundler, export the variable before running `bundle install`.
20
61
 
21
62
  ## Render Carve
22
63
 
@@ -28,6 +69,7 @@ Carve.to_markdown(source)
28
69
  Carve.to_plain_text(source)
29
70
  Carve.to_ansi(source)
30
71
  Carve.to_carve(source)
72
+ Carve.to_carve(source, align_tables: true) # pad table columns
31
73
  ```
32
74
 
33
75
  Options configure extensions, safe rendering, profiles, symbols, section
@@ -91,7 +133,9 @@ result = Carve.to_html_with_includes(
91
133
  )
92
134
  ```
93
135
 
94
- The result contains the value, warnings, and root-relative dependencies.
136
+ The result contains the value, warnings, and root-relative dependencies. A
137
+ warning from an included file names the chain of directives that reached it in
138
+ `:includedBy`.
95
139
  Other render methods leave directives literal. See the
96
140
  [include reference](docs/reference.md#file-includes) for budgets, AST expansion,
97
141
  targets, and containment behavior.
data/ext/carve/Cargo.lock CHANGED
@@ -37,9 +37,9 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
37
37
 
38
38
  [[package]]
39
39
  name = "carve-lang"
40
- version = "0.1.9"
40
+ version = "0.1.11"
41
41
  source = "registry+https://github.com/rust-lang/crates.io-index"
42
- checksum = "8747e096cacdd7097d884ffb9dd15dc70dd7e636081a31335bb786313924f341"
42
+ checksum = "753b90fbe80fab43887d5d559996c153347d9631452bbcb8d0283aad6dfa7ef8"
43
43
  dependencies = [
44
44
  "html5ever",
45
45
  "markup5ever_rcdom",
@@ -54,7 +54,7 @@ dependencies = [
54
54
 
55
55
  [[package]]
56
56
  name = "carve-rb"
57
- version = "0.1.8"
57
+ version = "0.1.10"
58
58
  dependencies = [
59
59
  "carve-lang",
60
60
  "magnus",
data/ext/carve/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "carve-rb"
3
- version = "0.1.8"
3
+ version = "0.1.10"
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", version = "=0.1.9" }
25
+ carve_rs = { package = "carve-lang", version = "=0.1.11" }
data/ext/carve/src/lib.rs CHANGED
@@ -27,7 +27,9 @@
27
27
  use carve_rs::extensions::registry;
28
28
  use carve_rs::{CarveExtension, Mode, Options, Profile, StaticRenderers};
29
29
  use magnus::value::{InnerValue, Opaque};
30
- use magnus::{function, prelude::*, Error, ExceptionClass, RArray, RHash, Ruby, Value};
30
+ use magnus::{
31
+ function, prelude::*, Error, ExceptionClass, RArray, RHash, RString, Ruby, TryConvert, Value,
32
+ };
31
33
  use std::any::Any;
32
34
  use std::cell::RefCell;
33
35
  use std::panic::{self, AssertUnwindSafe};
@@ -97,6 +99,14 @@ fn extension_names() -> Vec<String> {
97
99
  registry::keys().map(str::to_string).collect()
98
100
  }
99
101
 
102
+ fn ast_json_with_version(json: String) -> String {
103
+ let (body, closing) = json.split_at(json.len() - 1);
104
+ format!(
105
+ "{body},\"astVersion\":\"{}\"{closing}",
106
+ carve_rs::AST_CONTRACT_VERSION
107
+ )
108
+ }
109
+
100
110
  /// Map a Ruby-facing mode string to a carve-rs [`Mode`].
101
111
  ///
102
112
  /// Rejects any unknown string with `ArgumentError`, mirroring the spec's
@@ -216,8 +226,44 @@ fn to_ansi(source: String) -> String {
216
226
  carve_rs::to_ansi(&source)
217
227
  }
218
228
 
219
- fn to_carve(source: String) -> String {
220
- carve_rs::to_carve(&source)
229
+ /// Canonical Carve source; a writer refusal returns the source as authored.
230
+ fn to_carve(source: String, align_tables: bool) -> String {
231
+ let options = Options::default().with_align_tables(align_tables);
232
+ carve_rs::try_to_carve_with_options(&source, &options).unwrap_or(source)
233
+ }
234
+
235
+ /// Resolve `@region:name` with the engine's marker rules (spec I1b).
236
+ ///
237
+ /// Returns `[start, end]` (1-based, inclusive content lines) or, when the
238
+ /// region is invalid, the warning rule id as a String; `lib/carve.rb` turns
239
+ /// that into `Carve::IncludeRegionError`.
240
+ fn select_include_region(
241
+ ruby: &Ruby,
242
+ source: String,
243
+ name: String,
244
+ mode: String,
245
+ ) -> Result<Value, Error> {
246
+ let mode = match mode.as_str() {
247
+ "carve" => carve_rs::IncludeRegionMode::Carve,
248
+ "code" => carve_rs::IncludeRegionMode::Code,
249
+ other => {
250
+ return Err(Error::new(
251
+ ruby.exception_arg_error(),
252
+ format!("mode must be :carve or :code, got {other:?}"),
253
+ ))
254
+ }
255
+ };
256
+ Ok(
257
+ match carve_rs::select_include_region(&source, &name, mode) {
258
+ Ok(region) => ruby
259
+ .ary_new_from_values(&[
260
+ ruby.integer_from_u64(region.start as u64).as_value(),
261
+ ruby.integer_from_u64(region.end as u64).as_value(),
262
+ ])
263
+ .as_value(),
264
+ Err(e) => ruby.str_new(e.rule()).as_value(),
265
+ },
266
+ )
221
267
  }
222
268
 
223
269
  /// Which renderer runs over the expanded document.
@@ -334,6 +380,12 @@ fn render_with_includes_json(
334
380
  if !sections {
335
381
  render_options = render_options.with_sections(false);
336
382
  }
383
+ // Positions let the engine record the directive chain behind a warning
384
+ // (`includedBy`). The AST target keeps them off: spec I4 leaves position
385
+ // remapping out of scope, so a child's spans would point into the wrong file.
386
+ if which != IncludeTarget::Ast {
387
+ render_options = render_options.with_positions(true);
388
+ }
337
389
  let mut include_options = carve_rs::IncludeOptions::new()
338
390
  .with_resolver(&resolver)
339
391
  .with_source_path(source_real.to_string_lossy());
@@ -378,7 +430,7 @@ fn render_with_includes_json(
378
430
  // parse JSON twice. Positions stay OFF, unlike `to_ast_json`: spec I4
379
431
  // leaves position remapping out of scope in every engine, so a child's
380
432
  // spans would point into a document the caller never passed.
381
- IncludeTarget::Ast => Ok(carve_rs::to_json(&prepared.doc)),
433
+ IncludeTarget::Ast => Ok(ast_json_with_version(carve_rs::to_json(&prepared.doc))),
382
434
  }
383
435
  .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
384
436
  let value: serde_json::Value = if which == IncludeTarget::Ast {
@@ -409,6 +461,17 @@ fn render_with_includes_json(
409
461
  "rule": warning.rule,
410
462
  "message": warning.message,
411
463
  "file": warning.file.as_deref().map(&safe_path),
464
+ "includedBy": warning
465
+ .included_by
466
+ .iter()
467
+ .map(|site| {
468
+ serde_json::json!({
469
+ "file": safe_path(&site.file),
470
+ "line": site.line,
471
+ "column": site.column,
472
+ })
473
+ })
474
+ .collect::<Vec<_>>(),
412
475
  })
413
476
  })
414
477
  .collect::<Vec<_>>();
@@ -522,7 +585,9 @@ fn migration_diagnostic_json(diagnostic: &carve_rs::MigrationDiagnostic) -> serd
522
585
  fn to_ast_json(source: String) -> String {
523
586
  let mut options = Options::new();
524
587
  options.positions = true;
525
- carve_rs::to_json(&carve_rs::parse_with_options(&source, &options))
588
+ ast_json_with_version(carve_rs::to_json(&carve_rs::parse_with_options(
589
+ &source, &options,
590
+ )))
526
591
  }
527
592
 
528
593
  /// Render Carve source to HTML with the named extensions enabled.
@@ -842,42 +907,74 @@ macro_rules! guarded {
842
907
  ($wrapper:ident ( $($arg:ident : $ty:ty),* ) -> $ret:ty => $inner:ident) => {
843
908
  #[allow(clippy::too_many_arguments)]
844
909
  fn $wrapper($($arg: $ty),*) -> Result<$ret, Error> {
845
- guard(|| Ok($inner($($arg),*)))
910
+ guard(|| Ok($inner($($arg.into()),*)))
846
911
  }
847
912
  };
848
913
  ($wrapper:ident ( $($arg:ident : $ty:ty),* ) -> $ret:ty =>? $inner:ident) => {
849
914
  #[allow(clippy::too_many_arguments)]
850
915
  fn $wrapper($($arg: $ty),*) -> Result<$ret, Error> {
851
- guard(|| $inner($($arg),*))
916
+ guard(|| $inner($($arg.into()),*))
852
917
  }
853
918
  };
854
919
  }
855
920
 
856
- guarded!(g_to_html(source: String) -> String => to_html);
857
- guarded!(g_to_markdown(source: String) -> String => to_markdown);
858
- guarded!(g_to_plain_text(source: String) -> String => to_plain_text);
859
- guarded!(g_to_ansi(source: String) -> String => to_ansi);
860
- guarded!(g_to_carve(source: String) -> String => to_carve);
861
- guarded!(g_from_markdown_json(ruby: &Ruby, source: String) -> String =>? from_markdown_json);
862
- guarded!(g_to_ast_json(source: String) -> String => to_ast_json);
921
+ struct Source(String);
922
+
923
+ impl TryConvert for Source {
924
+ fn try_convert(value: Value) -> Result<Self, Error> {
925
+ let source = RString::try_convert(value)?;
926
+ let encoding: Value = source.funcall("encoding", ())?;
927
+ let name: String = encoding.funcall("name", ())?;
928
+ let text = if name == "ASCII-8BIT" {
929
+ String::from_utf8_lossy(unsafe { source.as_slice() }).into_owned()
930
+ } else {
931
+ let ruby = Ruby::get_with(source);
932
+ let options = ruby.hash_new();
933
+ options.aset(ruby.to_symbol("invalid"), ruby.to_symbol("replace"))?;
934
+ options.aset(ruby.to_symbol("undef"), ruby.to_symbol("replace"))?;
935
+ options.aset(ruby.to_symbol("replace"), "\u{FFFD}")?;
936
+ let encoded: RString = source.funcall("encode", ("UTF-8", magnus::KwArgs(options)))?;
937
+ encoded.to_string()?
938
+ };
939
+ Ok(Self(text))
940
+ }
941
+ }
942
+
943
+ impl From<Source> for String {
944
+ fn from(source: Source) -> Self {
945
+ source.0
946
+ }
947
+ }
948
+
949
+ guarded!(g_to_html(source: Source) -> String => to_html);
950
+ guarded!(g_to_markdown(source: Source) -> String => to_markdown);
951
+ guarded!(g_to_plain_text(source: Source) -> String => to_plain_text);
952
+ guarded!(g_to_ansi(source: Source) -> String => to_ansi);
953
+ guarded!(g_to_carve(source: Source, align_tables: bool) -> String => to_carve);
954
+ guarded!(
955
+ g_select_include_region(ruby: &Ruby, source: Source, name: String, mode: String) -> Value
956
+ =>? select_include_region
957
+ );
958
+ guarded!(g_from_markdown_json(ruby: &Ruby, source: Source) -> String =>? from_markdown_json);
959
+ guarded!(g_to_ast_json(source: Source) -> String => to_ast_json);
863
960
  guarded!(g_extension_names() -> Vec<String> => extension_names);
864
961
  guarded!(g_panic_probe() -> String => panic_probe);
865
962
  guarded!(
866
- g_stamp_needs_review(source: String, current_version: Option<String>) -> bool
963
+ g_stamp_needs_review(source: Source, current_version: Option<String>) -> bool
867
964
  => stamp_needs_review
868
965
  );
869
966
  guarded!(
870
- g_from_html_json(ruby: &Ruby, source: String, mode: String) -> String =>? from_html_json
967
+ g_from_html_json(ruby: &Ruby, source: Source, mode: String) -> String =>? from_html_json
871
968
  );
872
- guarded!(g_read_stamp(ruby: &Ruby, source: String) -> Value =>? read_stamp);
969
+ guarded!(g_read_stamp(ruby: &Ruby, source: Source) -> Value =>? read_stamp);
873
970
  guarded!(
874
- g_to_html_with_extensions(ruby: &Ruby, source: String, names: RArray) -> String
971
+ g_to_html_with_extensions(ruby: &Ruby, source: Source, names: RArray) -> String
875
972
  =>? to_html_with_extensions
876
973
  );
877
974
  guarded!(
878
975
  g_to_html_full(
879
976
  ruby: &Ruby,
880
- source: String,
977
+ source: Source,
881
978
  names: RArray,
882
979
  mode: String,
883
980
  renderers: RHash
@@ -886,7 +983,7 @@ guarded!(
886
983
  guarded!(
887
984
  g_to_html_full_with_symbols(
888
985
  ruby: &Ruby,
889
- source: String,
986
+ source: Source,
890
987
  names: RArray,
891
988
  mode: String,
892
989
  renderers: RHash,
@@ -896,7 +993,7 @@ guarded!(
896
993
  guarded!(
897
994
  g_to_html_safe(
898
995
  ruby: &Ruby,
899
- source: String,
996
+ source: Source,
900
997
  names: RArray,
901
998
  mode: String,
902
999
  renderers: RHash,
@@ -909,7 +1006,7 @@ guarded!(
909
1006
  guarded!(
910
1007
  g_render_with_includes_json(
911
1008
  ruby: &Ruby,
912
- source: String,
1009
+ source: Source,
913
1010
  root: String,
914
1011
  source_path: String,
915
1012
  target: String,
@@ -950,7 +1047,11 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
950
1047
  module.define_singleton_method("to_markdown", function!(g_to_markdown, 1))?;
951
1048
  module.define_singleton_method("to_plain_text", function!(g_to_plain_text, 1))?;
952
1049
  module.define_singleton_method("to_ansi", function!(g_to_ansi, 1))?;
953
- module.define_singleton_method("to_carve", function!(g_to_carve, 1))?;
1050
+ module.define_singleton_method("_to_carve", function!(g_to_carve, 2))?;
1051
+ module.define_singleton_method(
1052
+ "_select_include_region",
1053
+ function!(g_select_include_region, 3),
1054
+ )?;
954
1055
  module.define_singleton_method(
955
1056
  "_render_with_includes_json",
956
1057
  function!(g_render_with_includes_json, 15),
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.8"
11
+ VERSION = "0.1.10"
12
12
  end
data/lib/carve.rb CHANGED
@@ -8,11 +8,30 @@
8
8
  require "json"
9
9
  require_relative "carve/version"
10
10
 
11
- # Load the compiled native extension. Built by `rake compile` to
12
- # lib/carve/carve.so (or .bundle on macOS).
13
- require_relative "carve/carve"
11
+ # Load the compiled native extension. A precompiled platform gem ships one per
12
+ # Ruby ABI under lib/carve/<major.minor>/; a source install and `rake compile`
13
+ # build lib/carve/carve.so (or .bundle on macOS).
14
+ # Chosen by directory rather than by rescuing LoadError, so a binary that is
15
+ # present but cannot load (an older glibc, say) reports its own error.
16
+ abi = RUBY_VERSION[/\A\d+\.\d+/]
17
+ if File.directory?(File.join(__dir__, "carve", abi))
18
+ require_relative "carve/#{abi}/carve"
19
+ else
20
+ require_relative "carve/carve"
21
+ end
14
22
 
15
23
  module Carve
24
+ # Raised by .select_include_region when the region is missing or a marker is
25
+ # malformed, unpaired or duplicated. +rule+ is the engine's warning rule id.
26
+ class IncludeRegionError < ArgumentError
27
+ attr_reader :rule
28
+
29
+ def initialize(rule, name)
30
+ @rule = rule
31
+ super("#{rule}: region #{name.inspect} is missing or its markers are malformed")
32
+ end
33
+ end
34
+
16
35
  # Extension names the native binding understands.
17
36
  #
18
37
  # Taken from the engine itself rather than typed out here. This was a literal
@@ -55,7 +74,8 @@ module Carve
55
74
  #
56
75
  # Returns +{value:, warnings:, dependencies:, suppressedWarnings:}+. Paths
57
76
  # in +warnings+ and +dependencies+ are relative to the root, so a report can
58
- # be shown to a reader as it stands.
77
+ # be shown to a reader as it stands. Each warning's +includedBy+ lists the
78
+ # directives that reached its file, top-level first, as +{file:, line:, column:}+.
59
79
  #
60
80
  # Every render option +to_html+ takes is accepted and reaches the included
61
81
  # children too: an extension, a symbol map or a profile means the same thing
@@ -130,6 +150,28 @@ module Carve
130
150
  )
131
151
  end
132
152
 
153
+ # Format +source+ into canonical Carve. A writer refusal returns the source
154
+ # as authored. +align_tables:+ pads table cells into aligned columns.
155
+ def to_carve(source, align_tables: false)
156
+ _to_carve(source, align_tables ? true : false)
157
+ end
158
+
159
+ # Resolve <tt>@region:name</tt> in a file's text with the engine's marker
160
+ # rules, so an editor jumping to a region agrees with the render.
161
+ #
162
+ # Carve.select_include_region(text, "setup", mode: :carve) # => 3..4
163
+ #
164
+ # Returns the 1-based, inclusive Range of content lines; an empty region is
165
+ # an empty Range such as +2..1+. +mode: :carve+ accepts only
166
+ # <tt>%% #region</tt> markers, +mode: :code+ also the source-comment forms
167
+ # of an +@code+ include. Raises IncludeRegionError otherwise.
168
+ def select_include_region(source, name, mode:)
169
+ result = _select_include_region(source, name.to_s, mode.to_s)
170
+ raise IncludeRegionError.new(result, name.to_s) if result.is_a?(String)
171
+
172
+ result[0]..result[1]
173
+ end
174
+
133
175
  # Import HTML or Markdown into canonical Carve. Both methods return the
134
176
  # shared migration shape `{ value:, report: }`.
135
177
  def from_html(source, mode: :safe)
@@ -319,9 +361,10 @@ module Carve
319
361
  # # => {type: "document",
320
362
  # # children: [{type: "heading", level: 1,
321
363
  # # children: [{type: "text", value: "Hi"}], attrs: nil}],
322
- # # srcByteLength: 4}
364
+ # # srcByteLength: 4, astVersion: "1.2"}
323
365
  #
324
- # The root carries exactly +:type+, +:children+ and +:srcByteLength+ (spec
366
+ # The root carries exactly +:type+, +:children+, +:srcByteLength+ and
367
+ # +:astVersion+ (spec
325
368
  # PART 12 §7). Frontmatter and footnote definitions are block nodes in
326
369
  # +:children+, not root fields: a root field cannot carry the position §4
327
370
  # requires of every node, and both are source an editor navigates to.
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.8
4
+ version: 0.1.10
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-10-14 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys