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 +4 -4
- data/CHANGELOG.md +37 -1
- data/README.md +45 -37
- data/lib/carve/hexapdf/renderer.rb +10 -5
- data/lib/carve/hexapdf/version.rb +1 -1
- data/lib/carve/hexapdf.rb +79 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45c5c94de95a03c35c6a7ea92a22aff7fec66c96ff2a5dc44cb36a43034426d8
|
|
4
|
+
data.tar.gz: 0e3560d71885dd7476d999ab910a2ea0ba9b111c7dccb678979c238e26e9d63c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
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
|
-
|
|
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 "
|
|
570
|
-
when "
|
|
571
|
-
|
|
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
|
|
881
|
-
|
|
885
|
+
abbreviation footnote soft_break hard_break insert delete
|
|
886
|
+
substitution critic_comment
|
|
882
887
|
].freeze
|
|
883
888
|
end
|
|
884
889
|
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,
|
|
49
|
-
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-09-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: carve-lang
|