carve-hexapdf 0.1.0 → 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: d47fc98c98ecd21adba76ed8abb6150aabe535bcb99ad844b6f48705f115dce8
4
- data.tar.gz: 3213a192f274a65f6e262ac73009dda8d33d88945406b20096120ce38d5a6158
3
+ metadata.gz: 45c5c94de95a03c35c6a7ea92a22aff7fec66c96ff2a5dc44cb36a43034426d8
4
+ data.tar.gz: 0e3560d71885dd7476d999ab910a2ea0ba9b111c7dccb678979c238e26e9d63c
5
5
  SHA512:
6
- metadata.gz: 84ad78a47f2cbb33608373ed0d05e3af1f7a14b2f250ed193ad42586bce5e3bae4bdd0b58659517d58e410e6d5f2d7d4a739d9aacd167597c2ff1ffc44a5834d
7
- data.tar.gz: 8f88fe4e7cfb412788e3d4c045937db3a03707d891221561ab181485fc6026577cfbbc058c74334c1a5fb4cd07d38dce5d3a4b21bc1f98024142d8e42731ee89
6
+ metadata.gz: 45f2c67a07e0036a07d862bb7cd42641bfae6a8123b668ab6b910bd67dd54a78d49f4dbe0e83a8f8f25e432207391c0b9377e13d9abf1c2f56eee95b5cf151b8
7
+ data.tar.gz: b18adba4f2de1b2d2678baa7adb7ac15ce5d8d581b7ca561e186e3c2b33c0bf387b2ba58b6a336669cf33642772cbdd1cbadb06142567a5a827e53f0908f02f6
data/CHANGELOG.md CHANGED
@@ -5,6 +5,57 @@ 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
+ ## [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.
45
+
46
+ ## [0.1.1] - 2026-08-27
47
+
48
+ ### Changed
49
+
50
+ - The development pin moves to released `carve-lang` 0.1.2, so the suite covers
51
+ the current embedded engine and output surface. The supported consumer range
52
+ remains `>= 0.1.1, < 0.2.0`: 0.1.1 still satisfies this renderer's API and
53
+ security requirements. On the engine's own 0.x scheme `0.1` is the major and the third digit
54
+ the minor, so the previous open range admitted, by the engine's own rules, a
55
+ release declared breaking - and this gem reads an AST whose node vocabulary
56
+ was already renamed once inside 0.1
57
+ ([#26](https://github.com/markup-carve/carve-hexapdf/issues/26)).
58
+
8
59
  ## [0.1.0] - 2026-08-19
9
60
 
10
61
  ### Added
@@ -38,7 +89,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
38
89
  URLs show alt text, raw HTML and comments are dropped; the renderer never
39
90
  raises.
40
91
 
41
- [Unreleased]: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.0...HEAD
92
+ [Unreleased]: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.1...HEAD
93
+ [0.1.1]: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.0...v0.1.1
42
94
  [0.1.0]: https://github.com/markup-carve/carve-hexapdf/releases/tag/v0.1.0
43
95
 
44
96
  - **Composite figures render as one grouped float.** A bare `::: figure`
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 |
@@ -215,6 +259,10 @@ always produces a PDF:
215
259
  fetching. Local files and `data:` URIs are embedded.
216
260
  - **Raw HTML** blocks/inlines and **comments** are dropped.
217
261
 
262
+ ## Development
263
+
264
+ Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
265
+
218
266
  ## Licensing
219
267
 
220
268
  This gem is **MIT** licensed. However, **HexaPDF is dual-licensed AGPL-3.0 /
@@ -224,7 +272,3 @@ application) or hold a
224
272
  [HexaPDF commercial license](https://hexapdf.gettalong.org/#pricing). This gem
225
273
  only bridges Carve to HexaPDF; your use of HexaPDF is governed by HexaPDF's own
226
274
  terms.
227
-
228
- ## License
229
-
230
- MIT, markup-carve. See [LICENSE](LICENSE).
@@ -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.0"
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.0
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-08-19 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
@@ -17,6 +17,9 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 0.1.1
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 0.2.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -24,6 +27,9 @@ dependencies:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 0.1.1
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: hexapdf
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -107,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
113
  - !ruby/object:Gem::Version
108
114
  version: '0'
109
115
  requirements: []
110
- rubygems_version: 3.4.20
116
+ rubygems_version: 3.4.19
111
117
  signing_key:
112
118
  specification_version: 4
113
119
  summary: Render the Carve markup language to PDF via the pure-Ruby HexaPDF engine.