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 +4 -4
- data/CHANGELOG.md +53 -1
- data/README.md +48 -4
- data/lib/carve/hexapdf/renderer.rb +10 -5
- data/lib/carve/hexapdf/version.rb +1 -1
- data/lib/carve/hexapdf.rb +79 -2
- metadata +9 -3
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,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.
|
|
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 "
|
|
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-
|
|
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.
|
|
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.
|