carve-hexapdf 0.1.5 → 0.1.6
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 +16 -0
- data/README.md +9 -259
- data/lib/carve/hexapdf/renderer.rb +44 -11
- data/lib/carve/hexapdf/version.rb +1 -1
- 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: e542a29c024a850009d9f9fa32d6fe69ba870a97eab23f595522878d7da1c367
|
|
4
|
+
data.tar.gz: 05d84eaa30d5ad7c4d6f111dc442f88a113bf8a7aa817849df4ee7b58bb5942f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1382a76d094f651b2cc1b6cbae0ce65409d27222e02f4355361fce93ef6c9cc873b1ebd58ea01b6f02003ebb112ee8228784bb4dddf0aea60ac7186fc4e48f74
|
|
7
|
+
data.tar.gz: 25cd7967c9fe80a6c18df8a2d261c6d27d041a874029c056b52f43f621af5e97f939e3a19f9c141eaa51a87292829445ac58b8074a09a477c8afd61cff31b149
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.6] - 2026-11-12
|
|
11
|
+
|
|
12
|
+
Definition lists and table cell alignment now reach the PDF, the gem loads on Ruby 3.4, and the engine moves to carve-lang 0.1.14.
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
- Definition list terms and descriptions are drawn; they were dropped from the page on every engine (#66).
|
|
17
|
+
- `require "carve/hexapdf"` works on Ruby 3.4, where `base64` is no longer a default gem (#64).
|
|
18
|
+
|
|
19
|
+
### Improvements
|
|
20
|
+
|
|
21
|
+
- Table cells keep their horizontal alignment, from the cell or its column's header (#67).
|
|
22
|
+
- Developed and tested against carve-lang 0.1.14. Its braced alignment spelling (`|={>}`) right-aligns the column; a first-line `---` now opens frontmatter that runs to the end of an unclosed document, so nothing below it is drawn (#68).
|
|
23
|
+
|
|
24
|
+
**Full Changelog**: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.5...v0.1.6
|
|
25
|
+
|
|
10
26
|
## [0.1.5] - 2026-10-24
|
|
11
27
|
|
|
12
28
|
Ruby interchange text now appears in rendered PDFs, and tables with span markers render without a PDF layout error.
|
data/README.md
CHANGED
|
@@ -1,277 +1,27 @@
|
|
|
1
1
|
# carve-hexapdf
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**PDF** from Ruby, using the pure-Ruby [HexaPDF](https://hexapdf.gettalong.org)
|
|
5
|
-
document composition engine.
|
|
6
|
-
|
|
7
|
-
Carve source is parsed with [`Carve.parse`](https://github.com/markup-carve/carve-rb)
|
|
8
|
-
(from the `carve-lang` gem) and the resulting AST is walked onto a
|
|
9
|
-
`HexaPDF::Composer`:
|
|
10
|
-
|
|
11
|
-
- **Inline** nodes become HexaPDF styled text runs: `*strong*` and `/emphasis/`
|
|
12
|
-
map to bold / italic font variants, `` `code` `` to a monospace font, links to
|
|
13
|
-
a colored run with a clickable URI overlay.
|
|
14
|
-
- **Block** nodes map to HexaPDF boxes: headings and paragraphs to text boxes,
|
|
15
|
-
lists to list boxes (ordered / unordered / task), tables to table boxes,
|
|
16
|
-
block quotes / divs / admonitions to styled containers, and images to image
|
|
17
|
-
boxes.
|
|
3
|
+
Ruby PDF renderer for Carve documents. It supports `carve-lang` 0.1.1 or later and uses the HexaPDF composition engine. See the [Carve documentation](https://markup-carve.github.io/carve/).
|
|
18
4
|
|
|
19
5
|
## Install
|
|
20
6
|
|
|
21
|
-
```ruby
|
|
22
|
-
# Gemfile
|
|
23
|
-
gem "carve-hexapdf"
|
|
24
|
-
```
|
|
25
|
-
|
|
26
7
|
```sh
|
|
27
|
-
|
|
8
|
+
gem install carve-hexapdf
|
|
28
9
|
```
|
|
29
10
|
|
|
30
|
-
`carve-hexapdf` depends on `carve-lang` (a native gem that builds the Carve
|
|
31
|
-
engine via Rust) and on `hexapdf`.
|
|
32
|
-
|
|
33
11
|
## Usage
|
|
34
12
|
|
|
35
13
|
```ruby
|
|
36
14
|
require "carve/hexapdf"
|
|
37
|
-
|
|
38
|
-
# Carve syntax note: *...* is STRONG (bold), /.../ is EMPHASIS (italic).
|
|
39
|
-
pdf_bytes = Carve::Hexapdf.render(<<~CRV)
|
|
40
|
-
# Report
|
|
41
|
-
|
|
42
|
-
A paragraph with *bold*, /italic/, `code`, and a [link](https://example.com).
|
|
43
|
-
|
|
44
|
-
|= Name |= Score |
|
|
45
|
-
| Ann | 42 |
|
|
46
|
-
| Bob | 7 |
|
|
47
|
-
CRV
|
|
48
|
-
|
|
49
|
-
# Write straight to a file:
|
|
50
15
|
Carve::Hexapdf.render_file("# Hello", "hello.pdf")
|
|
51
|
-
|
|
52
|
-
# Render an already-parsed / transformed AST:
|
|
53
|
-
ast = Carve.parse("# From AST")
|
|
54
|
-
pdf_bytes = Carve::Hexapdf.render_ast(ast)
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
For interchange `ruby` nodes, PDF text shows each pair as
|
|
58
|
-
`base(annotation)`, including `()` when the annotation is empty.
|
|
59
|
-
|
|
60
|
-
### Includes
|
|
61
|
-
|
|
62
|
-
A Carve document can pull another file in with `{{ path }}`. A String has no
|
|
63
|
-
identity of its own, so a directive in one stays literal; name the file instead
|
|
64
|
-
and it expands:
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
pdf_bytes = Carve::Hexapdf.render_from_file("report/index.crv")
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`render_from_file` READS Carve from a path and hands back PDF bytes.
|
|
71
|
-
`render_file` is its opposite pair: it takes Carve source and WRITES the PDF out,
|
|
72
|
-
and a directive in that source stays literal.
|
|
73
|
-
|
|
74
|
-
Containment defaults to the input file's own directory, so a sibling or a file
|
|
75
|
-
below it resolves and nothing above it does. `include_root:` moves that root:
|
|
76
|
-
|
|
77
|
-
```ruby
|
|
78
|
-
Carve::Hexapdf.render_from_file("report/index.crv", include_root: "/srv/docs")
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
The root must be **absolute**. A relative one is refused rather than resolved,
|
|
82
|
-
because resolving it lands on whatever directory the process happens to run in,
|
|
83
|
-
which is not a root anyone chose. The named document has to sit inside the root.
|
|
84
|
-
|
|
85
|
-
A target that cannot be read leaves the directive drawn as written, and the
|
|
86
|
-
reason goes to stderr. The message does not say whether the file was missing or
|
|
87
|
-
refused by containment: both report `include-unresolved`, so a document cannot
|
|
88
|
-
be used to probe the filesystem. Pass `on_includes:` to take reporting over and
|
|
89
|
-
get the dependency identities with it:
|
|
90
|
-
|
|
91
|
-
```ruby
|
|
92
|
-
Carve::Hexapdf.render_from_file(
|
|
93
|
-
"report/index.crv",
|
|
94
|
-
on_includes: lambda { |warnings:, dependencies:, suppressed_warnings:|
|
|
95
|
-
dependencies.each { |d| puts "#{d[:path]} #{d[:resolved] ? 'read' : d[:denial]}" }
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`extensions:` and `profile:` reach the engine on the include path, and a child
|
|
101
|
-
file is parsed with the same ones as its parent. Without a root they raise,
|
|
102
|
-
rather than being dropped: `Carve.parse` accepts neither.
|
|
103
|
-
|
|
104
|
-
### Options
|
|
105
|
-
|
|
106
|
-
| Option | Default | Meaning |
|
|
107
|
-
| ------ | ------- | ------- |
|
|
108
|
-
| `page_size` | `:A4` | HexaPDF page size (e.g. `:A4`, `:Letter`) |
|
|
109
|
-
| `margin` | `45` | Page margin in points |
|
|
110
|
-
| `base_font` | `"Times"` | Proportional font family |
|
|
111
|
-
| `code_font` | `"Courier"` | Monospace font family |
|
|
112
|
-
| `link_color` | `"hp-blue"` | Fill color for links |
|
|
113
|
-
| `highlight_color` | `"fff3a3"` | Background color for `=highlight=` |
|
|
114
|
-
| `styles` | `nil` | Hierarchical style overrides (see below) |
|
|
115
|
-
| `renderers` | `nil` | Callables that turn math / diagram source into image bytes (see below) |
|
|
116
|
-
|
|
117
|
-
## Styling
|
|
118
|
-
|
|
119
|
-
Pass `styles:` to restyle renderer output without patching the renderer. Keys are
|
|
120
|
-
hierarchical dotted names; more specific entries win before parent entries, and
|
|
121
|
-
user values win over defaults at the same key.
|
|
122
|
-
|
|
123
|
-
```ruby
|
|
124
|
-
Carve::Hexapdf.render(source, styles: {
|
|
125
|
-
"heading" => { fill_color: "333333" },
|
|
126
|
-
"code.block" => { box: { background_color: "fff8dd", padding: 8 } },
|
|
127
|
-
"admonition.warning" => { box: { background_color: "fff0f0" } },
|
|
128
|
-
})
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
Resolution examples:
|
|
132
|
-
|
|
133
|
-
- `heading.1` resolves through `heading` and then `base`.
|
|
134
|
-
- `code.inline` resolves through `code` and then `base`.
|
|
135
|
-
- `admonition.warning` resolves through `admonition` and then `base`.
|
|
136
|
-
- `box:` hashes deep-merge; other values, including margin arrays, replace as a
|
|
137
|
-
whole.
|
|
138
|
-
- `box:` only takes effect on keys that draw a surrounding box (`code.block`,
|
|
139
|
-
`quote`, `admonition`, `definition_list`, `math`); on text-only keys it is
|
|
140
|
-
ignored.
|
|
141
|
-
- `list` accepts only its structural properties (`item_spacing`,
|
|
142
|
-
`content_indentation`); item text styling flows through `paragraph`.
|
|
143
|
-
|
|
144
|
-
Specificity comes first: `"heading" => { font_size: 30 }` does not override the
|
|
145
|
-
default `heading.1` size of `22`, but `"heading" => { fill_color: "333333" }`
|
|
146
|
-
does apply to all heading levels. To change all heading sizes, set
|
|
147
|
-
`heading.1` through `heading.6` individually.
|
|
148
|
-
|
|
149
|
-
Existing keyword options are sugar under `styles:` and explicit style entries
|
|
150
|
-
win: `base_font:` maps to `base.font`, `code_font:` to `code.font`,
|
|
151
|
-
`link_color:` to `link.fill_color`, and `highlight_color:` to
|
|
152
|
-
`highlight.background_color`.
|
|
153
|
-
|
|
154
|
-
| Key | Defaults |
|
|
155
|
-
| --- | -------- |
|
|
156
|
-
| `base` | `{ font: "Times" }` |
|
|
157
|
-
| `heading` | `{ margin: [10, 0, 6] }` |
|
|
158
|
-
| `heading.1` ... `heading.6` | `{ font_size: 22 }`, `{ font_size: 18 }`, `{ font_size: 15 }`, `{ font_size: 13 }`, `{ font_size: 12 }`, `{ font_size: 11 }` |
|
|
159
|
-
| `paragraph` | `{ margin: [0, 0, 8] }` |
|
|
160
|
-
| `code` | `{ font: "Courier" }` |
|
|
161
|
-
| `code.block` | `{ font_size: 9, margin: [2, 0, 8], box: { background_color: "f2f2f2", padding: 6 } }` |
|
|
162
|
-
| `code.inline` | `{}` |
|
|
163
|
-
| `quote` | `{ box: { margin: [2, 0, 8], padding: [4, 10], background_color: "f7f7f7" } }` |
|
|
164
|
-
| `admonition` | `{ box: { margin: [2, 0, 8], padding: [6, 10], background_color: "eef3fb" }, title_margin: [0, 0, 4] }` |
|
|
165
|
-
| `admonition.<kind>` | No defaults; any kind the parser accepts works (including hyphenated ones) |
|
|
166
|
-
| `list` | `{ item_spacing: 3, content_indentation: 18 }` |
|
|
167
|
-
| `definition_list` | `{ box: { margin: [0, 0, 8] }, definition_indent: 16 }` |
|
|
168
|
-
| `table` | `{ font_size: 10, cell_padding: 4, margin: [2, 0, 8] }` |
|
|
169
|
-
| `table.header` | `{}` |
|
|
170
|
-
| `table.caption` | `{ font_size: 9, margin: [0, 0, 8] }` |
|
|
171
|
-
| `figure.caption` | `{ font_size: 9, margin: [2, 0, 8], text_align: :center }` (panel captions too) |
|
|
172
|
-
| `figure.group` | `{ box: { margin: [2, 0, 8] }, column_gap: 18, min_column_width: 90 }` |
|
|
173
|
-
| `figure.group.caption` | `{ font_size: 9, margin: [4, 0, 0], text_align: :center }` |
|
|
174
|
-
| `footnote` | `{ font_size: 9, margin: [0, 0, 3] }` (endnote section entries) |
|
|
175
|
-
| `link` | `{ fill_color: "hp-blue" }` |
|
|
176
|
-
| `highlight` | `{ background_color: "fff3a3" }` |
|
|
177
|
-
| `image` | `{ margin: [2, 0, 8] }` |
|
|
178
|
-
| `math` | `{ font_size: 11, margin: [4, 0, 8], box: { padding: 4 } }` |
|
|
179
|
-
| `thematic_break` | `{ height: 2, margin: [8, 0, 8], background_color: "cccccc" }` |
|
|
180
|
-
|
|
181
|
-
## Supported constructs
|
|
182
|
-
|
|
183
|
-
Headings, paragraphs, all inline emphasis (strong / emphasis / bold-italic /
|
|
184
|
-
**underline** / **strikethrough** / **superscript** / **subscript** /
|
|
185
|
-
**highlight**), code, links, autolinks, soft & hard breaks, ordered / unordered
|
|
186
|
-
/ task lists (nested), **tables with header rows and full row / column spans**,
|
|
187
|
-
block quotes (with attribution), fenced code blocks, divs, admonitions,
|
|
188
|
-
definition lists, figures, thematic breaks, critic markup (insert → underline,
|
|
189
|
-
delete → strikethrough), **footnotes** (superscript `[n]` markers with the
|
|
190
|
-
bodies collected into a numbered endnote section - inline `^[..]` and
|
|
191
|
-
referenced `[^id]` alike), and **images** - both block and inline, embedded
|
|
192
|
-
from a local file path or a `data:` URI. Task-list checkboxes are drawn in the
|
|
193
|
-
list marker column, so item text and nested lists align like any other list.
|
|
194
|
-
|
|
195
|
-
### Composite figures
|
|
196
|
-
|
|
197
|
-
A bare `::: figure` container is one figure of ordered panels (Carve PART 9
|
|
198
|
-
section 4c):
|
|
199
|
-
|
|
200
|
-
```
|
|
201
|
-
{.columns-2}
|
|
202
|
-
::: figure
|
|
203
|
-

|
|
204
|
-
^ (a) One
|
|
205
|
-
|
|
206
|
-

|
|
207
|
-
^ (b) Two
|
|
208
|
-
:::
|
|
209
|
-
^ Figure #: Both samples
|
|
210
16
|
```
|
|
211
17
|
|
|
212
|
-
|
|
213
|
-
them and the group caption are laid out as a single box a page break may not
|
|
214
|
-
enter, and each panel keeps its own caption the same way - so the caption that
|
|
215
|
-
numbers the figure never lands on the page after the panels it numbers. A
|
|
216
|
-
group too tall to fit a page splits instead of failing the render, with the
|
|
217
|
-
panels still in source order.
|
|
218
|
-
|
|
219
|
-
`.columns-N` on the attribute line is honored when the page is wide enough to
|
|
220
|
-
give every column `figure.group.min_column_width` points, and is otherwise
|
|
221
|
-
ignored in favor of a stack. Every panel is drawn either way: the hint decides
|
|
222
|
-
arrangement, never content.
|
|
223
|
-
|
|
224
|
-
An opener that carries a title or a label (`::: figure "T"`, `::: figure [g]`)
|
|
225
|
-
is deliberately NOT this construct - it stays a generic container and renders
|
|
226
|
-
as one.
|
|
227
|
-
|
|
228
|
-
> [!NOTE]
|
|
229
|
-
> The parser this gem consumes (`carve-lang`, over carve-rs) does not produce
|
|
230
|
-
> `figure_group` nodes yet. The renderer accepts them today - through
|
|
231
|
-
> `Carve.parse` once the engine ships the construct, and through
|
|
232
|
-
> `Carve::Hexapdf.render_ast` with an AST from any engine that already does.
|
|
233
|
-
|
|
234
|
-
### Math and diagrams (renderer callables)
|
|
235
|
-
|
|
236
|
-
PDF has no client-side renderer, so math and diagram fences are turned into
|
|
237
|
-
embedded raster images through callables you supply in `renderers:`. Each
|
|
238
|
-
returns image bytes (PNG/JPG) as a String - or a Hash `{ bytes:, width:,
|
|
239
|
-
height: }` (points) to control the drawn size, so high-DPI rasters embed
|
|
240
|
-
crisply at their intended dimensions. A missing renderer, or one that returns
|
|
241
|
-
anything else or raises, degrades that construct to its monospace source.
|
|
242
|
-
|
|
243
|
-
```ruby
|
|
244
|
-
Carve::Hexapdf.render(source, renderers: {
|
|
245
|
-
# inline `$`x`$` and display `$$`x`$$` math:
|
|
246
|
-
math: ->(tex, display) { my_tex_to_png(tex, display) }, # -> bytes | {bytes:, width:, height:} | nil
|
|
247
|
-
# fenced ```mermaid / ```dot|graphviz / ```chart|vega:
|
|
248
|
-
mermaid: ->(src) { my_mermaid_to_png(src) },
|
|
249
|
-
graphviz: ->(src) { my_dot_to_png(src) },
|
|
250
|
-
chart: ->(src) { my_chart_to_png(src) },
|
|
251
|
-
})
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Graceful degradation
|
|
255
|
-
|
|
256
|
-
The renderer never raises on an unsupported node - it degrades so a document
|
|
257
|
-
always produces a PDF:
|
|
258
|
-
|
|
259
|
-
- **Math / diagram** fences without a matching `renderers:` callable render
|
|
260
|
-
their source in a monospace run.
|
|
261
|
-
- **Remote image URLs** (`http(s)://`) are shown as alt text - no network
|
|
262
|
-
fetching. Local files and `data:` URIs are embedded.
|
|
263
|
-
- **Raw HTML** blocks/inlines and **comments** are dropped.
|
|
264
|
-
|
|
265
|
-
## Development
|
|
18
|
+
## Documentation
|
|
266
19
|
|
|
267
|
-
|
|
20
|
+
- [Carve documentation](https://markup-carve.github.io/carve/)
|
|
21
|
+
- [Guide](docs/guide.md)
|
|
22
|
+
- [Documentation index](docs/README.md)
|
|
23
|
+
- [Contributing and development](docs/development.md)
|
|
268
24
|
|
|
269
|
-
##
|
|
25
|
+
## License
|
|
270
26
|
|
|
271
|
-
|
|
272
|
-
commercial**. If you distribute software or offer it over a network while
|
|
273
|
-
depending on HexaPDF, you must comply with the AGPL (open-source your
|
|
274
|
-
application) or hold a
|
|
275
|
-
[HexaPDF commercial license](https://hexapdf.gettalong.org/#pricing). This gem
|
|
276
|
-
only bridges Carve to HexaPDF; your use of HexaPDF is governed by HexaPDF's own
|
|
277
|
-
terms.
|
|
27
|
+
[MIT](LICENSE). HexaPDF has separate licensing terms; see the [guide](docs/guide.md#licensing).
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "stringio"
|
|
4
|
-
require "base64"
|
|
5
4
|
|
|
6
5
|
require_relative "style_map"
|
|
7
6
|
|
|
@@ -258,13 +257,15 @@ module Carve
|
|
|
258
257
|
def definition_list(node, target)
|
|
259
258
|
style = style_for("definition_list", with_box: true)
|
|
260
259
|
target.container(style: style[:box]) do |cont|
|
|
260
|
+
# `items` is a flat run of definition_term / definition_description
|
|
261
|
+
# nodes, not term/definition pairs.
|
|
261
262
|
Array(node[:items]).each do |item|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
263
|
+
case item[:type]
|
|
264
|
+
when "definition_term"
|
|
265
|
+
cont.formatted_text(inline_runs(item[:children], bold: true), margin: [2, 0, 1])
|
|
266
|
+
when "definition_description"
|
|
266
267
|
cont.container(style: { padding: [0, 0, 0, style[:definition_indent]] }) do |dcont|
|
|
267
|
-
Array(
|
|
268
|
+
Array(item[:children]).each { |ch| block(ch, dcont) }
|
|
268
269
|
end
|
|
269
270
|
end
|
|
270
271
|
end
|
|
@@ -434,6 +435,7 @@ module Carve
|
|
|
434
435
|
resolved.first.none? { |o| o[:row_span] > 1 } ? 1 : 0
|
|
435
436
|
table_style = style_for("table")
|
|
436
437
|
header_style = style_for("table.header")
|
|
438
|
+
columns = column_alignments(node)
|
|
437
439
|
|
|
438
440
|
cell_boxes = resolved.map do |row|
|
|
439
441
|
row.map do |o|
|
|
@@ -444,6 +446,9 @@ module Carve
|
|
|
444
446
|
# :margin belongs to the table box, :cell_padding is our pseudo-prop.
|
|
445
447
|
box_opts = cell_style.except(:margin, :cell_padding, :box)
|
|
446
448
|
box_opts[:padding] = cell_style[:cell_padding]
|
|
449
|
+
column = columns[o[:col]] || {}
|
|
450
|
+
align = o[:cell][:align] || column[:align]
|
|
451
|
+
box_opts[:text_align] = CELL_ALIGN[align] if CELL_ALIGN.key?(align)
|
|
447
452
|
box = @layout.formatted_text_box(runs, **box_opts)
|
|
448
453
|
hash = { content: box }
|
|
449
454
|
hash[:col_span] = o[:col_span] if o[:col_span] > 1
|
|
@@ -464,9 +469,37 @@ module Carve
|
|
|
464
469
|
end
|
|
465
470
|
end
|
|
466
471
|
|
|
472
|
+
CELL_ALIGN = { "left" => :left, "center" => :center, "right" => :right }.freeze
|
|
473
|
+
|
|
474
|
+
# A cell without its own alignment inherits its column's: the table's
|
|
475
|
+
# `columns` metadata, overridden by the head rows (the HTML `thead`), a
|
|
476
|
+
# later row winning. Any other cell's alignment is local. The head is
|
|
477
|
+
# `rowGroups.headRows` when published, else the leading header rows.
|
|
478
|
+
# Vertical alignment is not drawn: `text_valign` overflows a table cell.
|
|
479
|
+
def column_alignments(node)
|
|
480
|
+
columns = Array(node[:columns]).map { |c| (c || {}).slice(:align) }
|
|
481
|
+
rows = Array(node[:rows])
|
|
482
|
+
declared = node.dig(:rowGroups, :headRows)
|
|
483
|
+
head = declared.is_a?(Integer) ? rows.first(declared) : rows.take_while { |row| header_row?(row) }
|
|
484
|
+
head.each do |row|
|
|
485
|
+
Array(row[:cells]).each_with_index do |cell, col|
|
|
486
|
+
next if cell[:span]
|
|
487
|
+
|
|
488
|
+
(columns[col] ||= {})[:align] = cell[:align] if cell[:align]
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
columns
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# A `^` / `<` marker is not a header cell, but leaves its row in the head.
|
|
495
|
+
def header_row?(row)
|
|
496
|
+
content = Array(row[:cells]).reject { |cell| cell[:span] }
|
|
497
|
+
!content.empty? && content.all? { |cell| cell[:header] }
|
|
498
|
+
end
|
|
499
|
+
|
|
467
500
|
# Resolve Carve's explicit span markers (a `<` cell = merge left, a `^`
|
|
468
501
|
# cell = merge up) into per-cell col_span / row_span counts, returning
|
|
469
|
-
# rows of originator hashes {cell:, header:, col_span:, row_span:} with
|
|
502
|
+
# rows of originator hashes {cell:, header:, col:, col_span:, row_span:} with
|
|
470
503
|
# marker cells dropped. Every covered grid position is explicit in Carve,
|
|
471
504
|
# so a cell's column index equals its position in the row.
|
|
472
505
|
def resolve_spans(rows)
|
|
@@ -486,7 +519,7 @@ module Carve
|
|
|
486
519
|
else
|
|
487
520
|
# A leading marker has no cell to extend. Keep its grid slot
|
|
488
521
|
# so the table has a drawable cell in this row.
|
|
489
|
-
o = { cell: cell, header: cell[:header], col_span: 1, row_span: 1 }
|
|
522
|
+
o = { cell: cell, header: cell[:header], col: col, col_span: 1, row_span: 1 }
|
|
490
523
|
emitted << o
|
|
491
524
|
last = o
|
|
492
525
|
col_owner[col] = o
|
|
@@ -500,13 +533,13 @@ module Carve
|
|
|
500
533
|
bumped[owner.object_id] = true
|
|
501
534
|
end
|
|
502
535
|
if owner.nil?
|
|
503
|
-
o = { cell: cell, header: cell[:header], col_span: 1, row_span: 1 }
|
|
536
|
+
o = { cell: cell, header: cell[:header], col: col, col_span: 1, row_span: 1 }
|
|
504
537
|
emitted << o
|
|
505
538
|
col_owner[col] = o
|
|
506
539
|
end
|
|
507
540
|
# A valid owner's slot remains available for further `^` rows.
|
|
508
541
|
else
|
|
509
|
-
o = { cell: cell, header: cell[:header], col_span: 1, row_span: 1 }
|
|
542
|
+
o = { cell: cell, header: cell[:header], col: col, col_span: 1, row_span: 1 }
|
|
510
543
|
emitted << o
|
|
511
544
|
last = o
|
|
512
545
|
col_owner[col] = o
|
|
@@ -905,7 +938,7 @@ module Carve
|
|
|
905
938
|
meta, data = src.split(",", 2)
|
|
906
939
|
return nil unless data
|
|
907
940
|
|
|
908
|
-
bytes = meta.include?(";base64") ?
|
|
941
|
+
bytes = meta.include?(";base64") ? data.unpack1("m") : data
|
|
909
942
|
return StringIO.new(bytes)
|
|
910
943
|
end
|
|
911
944
|
return src if File.file?(src)
|
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.6
|
|
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-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: carve-lang
|