carve-hexapdf 0.1.1 → 0.1.2

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: d9b8c7c6b8002f3587048d1711e0901a180e4f41a85778a2e4654f7daaeed9da
4
- data.tar.gz: 6da20e9768485a0afe3aa2b71a73bb670a155a7adfb9e30d593bedead5db5632
3
+ metadata.gz: 45c5c94de95a03c35c6a7ea92a22aff7fec66c96ff2a5dc44cb36a43034426d8
4
+ data.tar.gz: 0e3560d71885dd7476d999ab910a2ea0ba9b111c7dccb678979c238e26e9d63c
5
5
  SHA512:
6
- metadata.gz: 41deae43d68f4996a26a2e2ccae432550bb730877b82a164b5203fe3020e14e515e85d0261ff25a603c92bc7cc1957c2494c8ac7cf9b2c4174eed686f8b639c3
7
- data.tar.gz: 3256a05ce59a8b4071df645e1df3690d3d9ee56c00d4f588f543330cf756c50e689c97aa39026db77e0caa1ac8f095b993fc57990dea20fecbb04d98fa838438
6
+ metadata.gz: 45f2c67a07e0036a07d862bb7cd42641bfae6a8123b668ab6b910bd67dd54a78d49f4dbe0e83a8f8f25e432207391c0b9377e13d9abf1c2f56eee95b5cf151b8
7
+ data.tar.gz: b18adba4f2de1b2d2678baa7adb7ac15ce5d8d581b7ca561e186e3c2b33c0bf387b2ba58b6a336669cf33642772cbdd1cbadb06142567a5a827e53f0908f02f6
data/CHANGELOG.md CHANGED
@@ -5,7 +5,43 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## Unreleased
8
+ ## [0.1.2] - 2026-09-19
9
+
10
+ ### Fixed
11
+
12
+ - **A substitution no longer vanishes from the page.** The inline renderer
13
+ dispatched on `critic_substitute` and read `new_text`; the engine publishes
14
+ `substitution`, whose halves are the inline arrays `old` and `new`
15
+ (markup-carve/carve-js#1827). The arm never fired, and the node has no
16
+ `children`, so the fallback rendered nothing at all. Both halves now reach the
17
+ page, struck for the replaced run and underlined for the replacement, matching
18
+ the reference ANSI and HTML renderers.
19
+ markup-carve/carve-hexapdf#39
20
+ - **Inserted and deleted runs are decorated again.** The same mismatch on two
21
+ more arms: the dispatch read `critic_insert` and `critic_delete` where the
22
+ engine publishes `insert` and `delete`. Their text still reached the page
23
+ through the fallback, so what was lost was the underline and the
24
+ strikethrough, leaving an editorial mark indistinguishable from body text.
25
+ markup-carve/carve-hexapdf#41
26
+
27
+ ### Added
28
+
29
+ - `Carve::Hexapdf.render_from_file(path)` renders the Carve document at `path`
30
+ with its `{{ path }}` includes expanded, contained to the file's own directory
31
+ by default and to `include_root:` when one is named. String input stays
32
+ literal. `on_includes:` hands the caller the warnings and the dependency
33
+ identities; without it the sanitized warnings go to stderr. `extensions:` and
34
+ `profile:` are accepted on that path and reach an included child.
35
+ markup-carve/carve-hexapdf#36
36
+
37
+ ### Changed
38
+
39
+ - The development engine pin moves to carve-rb `main`, past
40
+ markup-carve/carve-rb#130, which exposes `Carve.parse_with_includes`, carries
41
+ the render options into a child, and publishes the substitution halves as
42
+ inline arrays. The supported consumer range is unchanged: no released
43
+ `carve-lang` carries the new halves yet, so a consumer resolving through that
44
+ range still sees a substitution render as nothing until carve-rb publishes.
9
45
 
10
46
  ## [0.1.1] - 2026-08-27
11
47
 
data/README.md CHANGED
@@ -54,6 +54,50 @@ ast = Carve.parse("# From AST")
54
54
  pdf_bytes = Carve::Hexapdf.render_ast(ast)
55
55
  ```
56
56
 
57
+ ### Includes
58
+
59
+ A Carve document can pull another file in with `{{ path }}`. A String has no
60
+ identity of its own, so a directive in one stays literal; name the file instead
61
+ and it expands:
62
+
63
+ ```ruby
64
+ pdf_bytes = Carve::Hexapdf.render_from_file("report/index.crv")
65
+ ```
66
+
67
+ `render_from_file` READS Carve from a path and hands back PDF bytes.
68
+ `render_file` is its opposite pair: it takes Carve source and WRITES the PDF out,
69
+ and a directive in that source stays literal.
70
+
71
+ Containment defaults to the input file's own directory, so a sibling or a file
72
+ below it resolves and nothing above it does. `include_root:` moves that root:
73
+
74
+ ```ruby
75
+ Carve::Hexapdf.render_from_file("report/index.crv", include_root: "/srv/docs")
76
+ ```
77
+
78
+ The root must be **absolute**. A relative one is refused rather than resolved,
79
+ because resolving it lands on whatever directory the process happens to run in,
80
+ which is not a root anyone chose. The named document has to sit inside the root.
81
+
82
+ A target that cannot be read leaves the directive drawn as written, and the
83
+ reason goes to stderr. The message does not say whether the file was missing or
84
+ refused by containment: both report `include-unresolved`, so a document cannot
85
+ be used to probe the filesystem. Pass `on_includes:` to take reporting over and
86
+ get the dependency identities with it:
87
+
88
+ ```ruby
89
+ Carve::Hexapdf.render_from_file(
90
+ "report/index.crv",
91
+ on_includes: lambda { |warnings:, dependencies:, suppressed_warnings:|
92
+ dependencies.each { |d| puts "#{d[:path]} #{d[:resolved] ? 'read' : d[:denial]}" }
93
+ }
94
+ )
95
+ ```
96
+
97
+ `extensions:` and `profile:` reach the engine on the include path, and a child
98
+ file is parsed with the same ones as its parent. Without a root they raise,
99
+ rather than being dropped: `Carve.parse` accepts neither.
100
+
57
101
  ### Options
58
102
 
59
103
  | Option | Default | Meaning |
@@ -217,43 +261,7 @@ always produces a PDF:
217
261
 
218
262
  ## Development
219
263
 
220
- ```sh
221
- bundle install
222
- bundle exec rake test
223
- ```
224
-
225
- ### Which engine you are testing against
226
-
227
- `bundle install` here does **not** resolve `carve-lang` from RubyGems. `Gemfile`
228
- pins the engine to a carve-rb revision, so a development run measures one exact
229
- engine build rather than whatever the registry serves that day:
230
-
231
- ```sh
232
- bundle exec ruby script/verify_engine_pin.rb
233
- # carve-lang 0.1.2 from carve-rb b7f3a91a... (Gemfile pins b7f3a91a4192)
234
- ```
235
-
236
- CI runs that before the suite and refuses when the Gemfile, the resolved bundle
237
- and the loaded library disagree. To develop against a local carve-rb checkout,
238
- point `CARVE_RB_PATH` at it - it has to be set on purpose, so the surprising
239
- resolution is the one you asked for:
240
-
241
- ```sh
242
- CARVE_RB_PATH=../carve-rb bundle install
243
- ```
244
-
245
- An installed copy of this gem resolves differently, through the range
246
- `carve-hexapdf.gemspec` declares - today `>= 0.1.1, < 0.2.0`. The two are
247
- deliberately not the same engine, and the difference is not academic: this gem
248
- reads an AST whose node vocabulary was renamed once already inside 0.1.
249
- `test/engine_floor_test.rb` asks whether the engine under `bundle exec rake
250
- test` still publishes the vocabulary the renderer maps, and
251
- `script/consumer_engine_probe.rb` asks the same of an actual `gem install` -
252
- daily in `Engine drift`, and again before every release.
253
-
254
- There is no committed `Gemfile.lock`, and that is deliberate; the reasoning,
255
- including why a lockfile would be a weaker pin here and what it would cost the
256
- Ruby 3.1 job, is at the top of `Gemfile`.
264
+ Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
257
265
 
258
266
  ## Licensing
259
267
 
@@ -566,9 +566,14 @@ module Carve
566
566
  when "abbreviation" then out << run(node[:abbr].to_s, ctx)
567
567
  when "cross_ref" then out << run(node[:target].to_s, ctx)
568
568
  when "caption_number" then (out << run(node[:number].to_s, ctx) if node[:number])
569
- when "critic_insert" then emit_children(node, ctx.merge(underline: true), out)
570
- when "critic_delete" then emit_children(node, ctx.merge(strike: true), out)
571
- when "critic_substitute" then out << run(node[:new_text].to_s, ctx.merge(underline: true))
569
+ when "insert" then emit_children(node, ctx.merge(underline: true), out)
570
+ when "delete" then emit_children(node, ctx.merge(strike: true), out)
571
+ # Both halves are inline arrays, and both reach the page: struck for the
572
+ # replaced run, underlined for the replacement, which is what the
573
+ # reference ANSI and HTML renderers show.
574
+ when "substitution"
575
+ emit_children({ children: node[:old] }, ctx.merge(strike: true), out)
576
+ emit_children({ children: node[:new] }, ctx.merge(underline: true), out)
572
577
  when "smart_punctuation" then out << run(smart_punctuation_text(node), ctx)
573
578
  # A character the author escaped. The backslash is authoring syntax, so
574
579
  # the page shows the character - but the node has no children, so
@@ -877,8 +882,8 @@ module Carve
877
882
  INLINE_TYPES = %w[
878
883
  text emphasis code link image span math raw_inline emoji autolink
879
884
  cross_ref caption_number mention tag citation_group inline_extension
880
- abbreviation footnote soft_break hard_break critic_insert critic_delete
881
- critic_substitute critic_comment
885
+ abbreviation footnote soft_break hard_break insert delete
886
+ substitution critic_comment
882
887
  ].freeze
883
888
  end
884
889
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Carve
4
4
  module Hexapdf
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
data/lib/carve/hexapdf.rb CHANGED
@@ -45,8 +45,31 @@ module Carve
45
45
  # +:bytes+ and optional +:width+/+:height+ (points) to
46
46
  # control the drawn size; anything else degrades the
47
47
  # construct to its monospace source.
48
- def render(source, **opts)
49
- render_ast(::Carve.parse(source), **opts)
48
+ def render(source, include_root: nil, source_path: nil, extensions: nil,
49
+ profile: nil, on_includes: nil, **opts)
50
+ ast = parse_source(source, include_root: include_root, source_path: source_path,
51
+ extensions: extensions, profile: profile,
52
+ on_includes: on_includes)
53
+ render_ast(ast, **opts)
54
+ end
55
+
56
+ # Render the Carve document at +path+, with its <tt>{{ path }}</tt>
57
+ # includes expanded, and return the PDF bytes.
58
+ #
59
+ # Carve::Hexapdf.render_from_file("report/index.crv")
60
+ #
61
+ # NOT to be confused with {render_file}, which takes Carve SOURCE and
62
+ # writes the PDF out. This one READS the Carve and hands the bytes back.
63
+ #
64
+ # Containment defaults to the input file's own directory, so a sibling or
65
+ # a file below it resolves and nothing above it does. +include_root:+
66
+ # widens or moves that root, and reaches the engine as the caller wrote
67
+ # it: the engine refuses a relative root, which is what keeps containment
68
+ # off whatever directory the process happens to run in.
69
+ def render_from_file(path, include_root: nil, **opts)
70
+ absolute = File.expand_path(path)
71
+ root = include_root || File.dirname(absolute)
72
+ render(File.read(absolute), include_root: root, source_path: absolute, **opts)
50
73
  end
51
74
 
52
75
  # Render an already-parsed Carve AST Hash (see +Carve.parse+) to PDF
@@ -62,10 +85,64 @@ module Carve
62
85
  end
63
86
 
64
87
  # Render Carve +source+ and write the PDF to +path+. Returns +path+.
88
+ #
89
+ # +source+ is a String with no identity of its own, so a directive stays
90
+ # literal unless the caller names both +include_root:+ and +source_path:+.
65
91
  def render_file(source, path, **opts)
66
92
  File.binwrite(path, render(source, **opts))
67
93
  path
68
94
  end
95
+
96
+ private
97
+
98
+ # The AST to draw: expanded when the caller named a root and a document.
99
+ #
100
+ # A String reaching this with neither is parsed as it stands, so a
101
+ # directive in it is text. That is the only thing a caller with no file
102
+ # can be given: a relative include has nothing to resolve against.
103
+ def parse_source(source, include_root:, source_path:, extensions:, profile:,
104
+ on_includes:)
105
+ if include_root.nil? || source_path.nil?
106
+ unless extensions.nil? && profile.nil?
107
+ raise ArgumentError,
108
+ "extensions: and profile: reach the engine on the include path only; " \
109
+ "pass include_root: and source_path:, or drop them"
110
+ end
111
+
112
+ return ::Carve.parse(source)
113
+ end
114
+
115
+ result = ::Carve.parse_with_includes(source, root: include_root,
116
+ source_path: source_path,
117
+ extensions: extensions,
118
+ profile: profile)
119
+ report_includes(result, on_includes)
120
+ result[:value]
121
+ end
122
+
123
+ # Hand the caller everything expansion found, or say what degraded.
124
+ #
125
+ # With +on_includes:+ the caller owns reporting and gets the dependency
126
+ # identities with it. Without it the sanitized warnings go to stderr,
127
+ # because a document that silently lost half its content is the failure
128
+ # this exists to prevent.
129
+ def report_includes(result, on_includes)
130
+ if on_includes
131
+ on_includes.call(warnings: Array(result[:warnings]),
132
+ dependencies: Array(result[:dependencies]),
133
+ suppressed_warnings: result[:suppressedWarnings].to_i)
134
+ return
135
+ end
136
+
137
+ Array(result[:warnings]).each do |warning|
138
+ where = warning[:file] ? "#{warning[:file]}: " : ""
139
+ $stderr.puts "carve-hexapdf: #{where}#{warning[:rule]}: #{warning[:message]}"
140
+ end
141
+ suppressed = result[:suppressedWarnings].to_i
142
+ return unless suppressed.positive?
143
+
144
+ $stderr.puts "carve-hexapdf: #{suppressed} further include warnings suppressed"
145
+ end
69
146
  end
70
147
  end
71
148
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carve-hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
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-09 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: carve-lang