carve-hexapdf 0.1.0
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 +7 -0
- data/CHANGELOG.md +119 -0
- data/LICENSE +21 -0
- data/README.md +230 -0
- data/lib/carve/hexapdf/renderer.rb +885 -0
- data/lib/carve/hexapdf/style_map.rb +159 -0
- data/lib/carve/hexapdf/version.rb +7 -0
- data/lib/carve/hexapdf.rb +71 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d47fc98c98ecd21adba76ed8abb6150aabe535bcb99ad844b6f48705f115dce8
|
|
4
|
+
data.tar.gz: 3213a192f274a65f6e262ac73009dda8d33d88945406b20096120ce38d5a6158
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 84ad78a47f2cbb33608373ed0d05e3af1f7a14b2f250ed193ad42586bce5e3bae4bdd0b58659517d58e410e6d5f2d7d4a739d9aacd167597c2ff1ffc44a5834d
|
|
7
|
+
data.tar.gz: 8f88fe4e7cfb412788e3d4c045937db3a03707d891221561ab181485fc6026577cfbbc058c74334c1a5fb4cd07d38dce5d3a4b21bc1f98024142d8e42731ee89
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-08-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release: `Carve::Hexapdf.render`, `.render_ast`, and `.render_file`
|
|
13
|
+
to render Carve markup to PDF via the pure-Ruby HexaPDF engine.
|
|
14
|
+
- AST walker mapping Carve blocks to HexaPDF text/list/table/container/image
|
|
15
|
+
boxes and Carve inline nodes to styled text runs.
|
|
16
|
+
- All inline emphasis renders with its decoration: strong/italic/bold-italic
|
|
17
|
+
(font variants), underline, strikethrough, superscript, subscript, and
|
|
18
|
+
highlight; critic insert/delete map to underline/strikethrough; footnote
|
|
19
|
+
references render as superscript.
|
|
20
|
+
- Tables with header rows and full **row / column span** resolution (`^` / `<`
|
|
21
|
+
markers map to HexaPDF `row_span` / `col_span`).
|
|
22
|
+
- Math and diagram fences render as embedded raster images via optional
|
|
23
|
+
`renderers:` callables (`:math`, `:mermaid`, `:graphviz`, `:chart`), degrading
|
|
24
|
+
to monospace source when no renderer is supplied.
|
|
25
|
+
- Images (block and inline) embedded from local file paths or `data:` URIs.
|
|
26
|
+
- Footnotes (inline `^[..]` and referenced `[^id]`) render as superscript
|
|
27
|
+
`[n]` markers with their bodies collected into a numbered endnote section.
|
|
28
|
+
- Task-list checkboxes are drawn in the list marker column, so item text and
|
|
29
|
+
nested lists align like any other list.
|
|
30
|
+
- Renderer callables may return `{ bytes:, width:, height: }` to control the
|
|
31
|
+
drawn size, so high-DPI rasters embed crisply.
|
|
32
|
+
- Hierarchical `styles:` support through `Carve::Hexapdf::StyleMap` for
|
|
33
|
+
restyling headings, code, links, highlights, admonitions, tables, images,
|
|
34
|
+
math fallbacks, and other renderer surfaces; the `base_font:`, `code_font:`,
|
|
35
|
+
`link_color:`, and `highlight_color:` keyword options are convenience sugar
|
|
36
|
+
under the style map.
|
|
37
|
+
- Graceful degradation: unknown nodes fall back to text/children, remote image
|
|
38
|
+
URLs show alt text, raw HTML and comments are dropped; the renderer never
|
|
39
|
+
raises.
|
|
40
|
+
|
|
41
|
+
[Unreleased]: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.0...HEAD
|
|
42
|
+
[0.1.0]: https://github.com/markup-carve/carve-hexapdf/releases/tag/v0.1.0
|
|
43
|
+
|
|
44
|
+
- **Composite figures render as one grouped float.** A bare `::: figure`
|
|
45
|
+
container is one numbered figure of ordered panels (Carve PART 9 section 4c,
|
|
46
|
+
AST node `figure_group`). Its panels, the content preserved between them and
|
|
47
|
+
the group caption are laid out as a single box a page break may not enter,
|
|
48
|
+
and each panel keeps its own caption the same way, so the caption that numbers
|
|
49
|
+
the figure cannot land on the page after it. A group too tall for a page
|
|
50
|
+
splits rather than failing the render, panels still in source order. A
|
|
51
|
+
`.columns-N` hint is honored when the page can give every column
|
|
52
|
+
`figure.group.min_column_width` points and is otherwise ignored in favor of a
|
|
53
|
+
stack - every panel is drawn either way. Two new style keys, `figure.group`
|
|
54
|
+
and `figure.group.caption`. The parser this gem consumes does not produce the
|
|
55
|
+
node yet; `render_ast` accepts one from any engine that does.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- **The `carve-lang` floor moves to `>= 0.1.1`.** At `>= 0.1` a consumer could
|
|
60
|
+
resolve carve-lang 0.1.0, which predates the Carve 0.1.3 security release and
|
|
61
|
+
renders a list-valued URL attribute unsanitized - so this gem's own suite
|
|
62
|
+
could be green while every install of it carried a vulnerable engine. 0.1.1 is
|
|
63
|
+
the rebuild onto carve-rs 0.1.3.
|
|
64
|
+
|
|
65
|
+
- **A table's caption no longer loses the whole document.** `table.caption`
|
|
66
|
+
sits under `table` in the style chain, so it inherited `cell_padding` - a
|
|
67
|
+
property this renderer reads itself and HexaPDF has never heard of - and
|
|
68
|
+
splatting it into the caption's text box raised `NoMethodError`. Every
|
|
69
|
+
captioned table was affected; no fixture had captioned one.
|
|
70
|
+
|
|
71
|
+
- **A resolved no-break space renders instead of killing the render.** The
|
|
72
|
+
engines publish U+E000 in a text value to stand for a no-break space the parser
|
|
73
|
+
resolved - from an escaped space (`a\ b`) or a line block's preserved
|
|
74
|
+
indentation - and PART 12 says a consumer maps it to its target's no-break
|
|
75
|
+
space and must not emit it. It was drawn as-is, and the default Type1 font has
|
|
76
|
+
no glyph for a private-use codepoint, so an ordinary escaped space raised
|
|
77
|
+
`HexaPDF::MissingGlyphError` and produced no document at all. It now maps to
|
|
78
|
+
U+00A0 in text, inline code, code blocks and image alt text.
|
|
79
|
+
|
|
80
|
+
- **Four more inline nodes reach the page instead of vanishing.** `symbol`,
|
|
81
|
+
`literal_inline`, `caption_number` and `inline_extension` each carry their
|
|
82
|
+
content in `name`, `content` or `n` rather than in `children`, and the inline
|
|
83
|
+
fallback emits children only - so they contributed nothing at all: no error, no
|
|
84
|
+
placeholder, just missing text. A captioned figure rendered its caption without
|
|
85
|
+
its number. The `emoji` arm had also gone dead when that type was renamed to
|
|
86
|
+
`symbol` upstream; both names are accepted now. What each prints follows the
|
|
87
|
+
reference engine's plain-text target. `heading_ref` and `substitution` are still
|
|
88
|
+
dropped and are pinned as known gaps, since each needs a decision rather than an
|
|
89
|
+
arm.
|
|
90
|
+
|
|
91
|
+
- **Bold, italic, underline, strike, superscript and subscript reach the page
|
|
92
|
+
again.** Each emphasis sort is its own node type now, not one `emphasis` node
|
|
93
|
+
carrying a `kind` (carve-rb#32). The renderer matched only `emphasis`, so all
|
|
94
|
+
seven fell through to the default branch: the text still rendered, in the
|
|
95
|
+
regular face with no decoration, and the PDF stayed valid - which is why the
|
|
96
|
+
smoke test kept passing. `/*both*/` is a single `strong` carrying
|
|
97
|
+
`boldItalic`, so the italic is read off the flag rather than inferred from
|
|
98
|
+
nesting.
|
|
99
|
+
|
|
100
|
+
- **Footnote endnote numbers are back, and the body appears once.** Definitions
|
|
101
|
+
used to arrive as a `footnote_defs` map on the root; PART 12 fixes the root at
|
|
102
|
+
three fields, so they moved into the tree as `footnote` block nodes carrying a
|
|
103
|
+
`label` (carve-rb#19, #21), and the inline became `footnote_ref` /
|
|
104
|
+
`inline_footnote` rather than `footnote`. Reading the old root field found
|
|
105
|
+
nothing, every reference failed to resolve, and the `[1]` markers silently
|
|
106
|
+
vanished while the bodies still rendered. A definition is now relocated to the
|
|
107
|
+
endnote section wherever it sits, including inside a container.
|
|
108
|
+
|
|
109
|
+
Both old spellings are still accepted, so an AST stored by an earlier version
|
|
110
|
+
renders the same.
|
|
111
|
+
|
|
112
|
+
- **An escaped character no longer vanishes from the page.** `escaped_text` is
|
|
113
|
+
its own inline node (carve#350), and it carries no children - so without an
|
|
114
|
+
arm of its own it fell through to the branch that emits children and rendered
|
|
115
|
+
nothing at all. `a\-b` came out as `ab`. The same silent-drop shape as the
|
|
116
|
+
smart-punctuation node before it (carve#355).
|
|
117
|
+
|
|
118
|
+
Dormant until the engine behind `Carve.parse` emits the node; handled here
|
|
119
|
+
first so the bump lands into a renderer that already copes.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 markup-carve
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# carve-hexapdf
|
|
2
|
+
|
|
3
|
+
Render the [Carve](https://github.com/markup-carve/carve) markup language to
|
|
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.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Gemfile
|
|
23
|
+
gem "carve-hexapdf"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`carve-hexapdf` depends on `carve-lang` (a native gem that builds the Carve
|
|
31
|
+
engine via Rust) and on `hexapdf`.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
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
|
+
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
|
+
### Options
|
|
58
|
+
|
|
59
|
+
| Option | Default | Meaning |
|
|
60
|
+
| ------ | ------- | ------- |
|
|
61
|
+
| `page_size` | `:A4` | HexaPDF page size (e.g. `:A4`, `:Letter`) |
|
|
62
|
+
| `margin` | `45` | Page margin in points |
|
|
63
|
+
| `base_font` | `"Times"` | Proportional font family |
|
|
64
|
+
| `code_font` | `"Courier"` | Monospace font family |
|
|
65
|
+
| `link_color` | `"hp-blue"` | Fill color for links |
|
|
66
|
+
| `highlight_color` | `"fff3a3"` | Background color for `=highlight=` |
|
|
67
|
+
| `styles` | `nil` | Hierarchical style overrides (see below) |
|
|
68
|
+
| `renderers` | `nil` | Callables that turn math / diagram source into image bytes (see below) |
|
|
69
|
+
|
|
70
|
+
## Styling
|
|
71
|
+
|
|
72
|
+
Pass `styles:` to restyle renderer output without patching the renderer. Keys are
|
|
73
|
+
hierarchical dotted names; more specific entries win before parent entries, and
|
|
74
|
+
user values win over defaults at the same key.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Carve::Hexapdf.render(source, styles: {
|
|
78
|
+
"heading" => { fill_color: "333333" },
|
|
79
|
+
"code.block" => { box: { background_color: "fff8dd", padding: 8 } },
|
|
80
|
+
"admonition.warning" => { box: { background_color: "fff0f0" } },
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Resolution examples:
|
|
85
|
+
|
|
86
|
+
- `heading.1` resolves through `heading` and then `base`.
|
|
87
|
+
- `code.inline` resolves through `code` and then `base`.
|
|
88
|
+
- `admonition.warning` resolves through `admonition` and then `base`.
|
|
89
|
+
- `box:` hashes deep-merge; other values, including margin arrays, replace as a
|
|
90
|
+
whole.
|
|
91
|
+
- `box:` only takes effect on keys that draw a surrounding box (`code.block`,
|
|
92
|
+
`quote`, `admonition`, `definition_list`, `math`); on text-only keys it is
|
|
93
|
+
ignored.
|
|
94
|
+
- `list` accepts only its structural properties (`item_spacing`,
|
|
95
|
+
`content_indentation`); item text styling flows through `paragraph`.
|
|
96
|
+
|
|
97
|
+
Specificity comes first: `"heading" => { font_size: 30 }` does not override the
|
|
98
|
+
default `heading.1` size of `22`, but `"heading" => { fill_color: "333333" }`
|
|
99
|
+
does apply to all heading levels. To change all heading sizes, set
|
|
100
|
+
`heading.1` through `heading.6` individually.
|
|
101
|
+
|
|
102
|
+
Existing keyword options are sugar under `styles:` and explicit style entries
|
|
103
|
+
win: `base_font:` maps to `base.font`, `code_font:` to `code.font`,
|
|
104
|
+
`link_color:` to `link.fill_color`, and `highlight_color:` to
|
|
105
|
+
`highlight.background_color`.
|
|
106
|
+
|
|
107
|
+
| Key | Defaults |
|
|
108
|
+
| --- | -------- |
|
|
109
|
+
| `base` | `{ font: "Times" }` |
|
|
110
|
+
| `heading` | `{ margin: [10, 0, 6] }` |
|
|
111
|
+
| `heading.1` ... `heading.6` | `{ font_size: 22 }`, `{ font_size: 18 }`, `{ font_size: 15 }`, `{ font_size: 13 }`, `{ font_size: 12 }`, `{ font_size: 11 }` |
|
|
112
|
+
| `paragraph` | `{ margin: [0, 0, 8] }` |
|
|
113
|
+
| `code` | `{ font: "Courier" }` |
|
|
114
|
+
| `code.block` | `{ font_size: 9, margin: [2, 0, 8], box: { background_color: "f2f2f2", padding: 6 } }` |
|
|
115
|
+
| `code.inline` | `{}` |
|
|
116
|
+
| `quote` | `{ box: { margin: [2, 0, 8], padding: [4, 10], background_color: "f7f7f7" } }` |
|
|
117
|
+
| `admonition` | `{ box: { margin: [2, 0, 8], padding: [6, 10], background_color: "eef3fb" }, title_margin: [0, 0, 4] }` |
|
|
118
|
+
| `admonition.<kind>` | No defaults; any kind the parser accepts works (including hyphenated ones) |
|
|
119
|
+
| `list` | `{ item_spacing: 3, content_indentation: 18 }` |
|
|
120
|
+
| `definition_list` | `{ box: { margin: [0, 0, 8] }, definition_indent: 16 }` |
|
|
121
|
+
| `table` | `{ font_size: 10, cell_padding: 4, margin: [2, 0, 8] }` |
|
|
122
|
+
| `table.header` | `{}` |
|
|
123
|
+
| `table.caption` | `{ font_size: 9, margin: [0, 0, 8] }` |
|
|
124
|
+
| `figure.caption` | `{ font_size: 9, margin: [2, 0, 8], text_align: :center }` (panel captions too) |
|
|
125
|
+
| `figure.group` | `{ box: { margin: [2, 0, 8] }, column_gap: 18, min_column_width: 90 }` |
|
|
126
|
+
| `figure.group.caption` | `{ font_size: 9, margin: [4, 0, 0], text_align: :center }` |
|
|
127
|
+
| `footnote` | `{ font_size: 9, margin: [0, 0, 3] }` (endnote section entries) |
|
|
128
|
+
| `link` | `{ fill_color: "hp-blue" }` |
|
|
129
|
+
| `highlight` | `{ background_color: "fff3a3" }` |
|
|
130
|
+
| `image` | `{ margin: [2, 0, 8] }` |
|
|
131
|
+
| `math` | `{ font_size: 11, margin: [4, 0, 8], box: { padding: 4 } }` |
|
|
132
|
+
| `thematic_break` | `{ height: 2, margin: [8, 0, 8], background_color: "cccccc" }` |
|
|
133
|
+
|
|
134
|
+
## Supported constructs
|
|
135
|
+
|
|
136
|
+
Headings, paragraphs, all inline emphasis (strong / emphasis / bold-italic /
|
|
137
|
+
**underline** / **strikethrough** / **superscript** / **subscript** /
|
|
138
|
+
**highlight**), code, links, autolinks, soft & hard breaks, ordered / unordered
|
|
139
|
+
/ task lists (nested), **tables with header rows and full row / column spans**,
|
|
140
|
+
block quotes (with attribution), fenced code blocks, divs, admonitions,
|
|
141
|
+
definition lists, figures, thematic breaks, critic markup (insert → underline,
|
|
142
|
+
delete → strikethrough), **footnotes** (superscript `[n]` markers with the
|
|
143
|
+
bodies collected into a numbered endnote section - inline `^[..]` and
|
|
144
|
+
referenced `[^id]` alike), and **images** - both block and inline, embedded
|
|
145
|
+
from a local file path or a `data:` URI. Task-list checkboxes are drawn in the
|
|
146
|
+
list marker column, so item text and nested lists align like any other list.
|
|
147
|
+
|
|
148
|
+
### Composite figures
|
|
149
|
+
|
|
150
|
+
A bare `::: figure` container is one figure of ordered panels (Carve PART 9
|
|
151
|
+
section 4c):
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
{.columns-2}
|
|
155
|
+
::: figure
|
|
156
|
+

|
|
157
|
+
^ (a) One
|
|
158
|
+
|
|
159
|
+

|
|
160
|
+
^ (b) Two
|
|
161
|
+
:::
|
|
162
|
+
^ Figure #: Both samples
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
On a page that group is one float. Its panels, any content preserved between
|
|
166
|
+
them and the group caption are laid out as a single box a page break may not
|
|
167
|
+
enter, and each panel keeps its own caption the same way - so the caption that
|
|
168
|
+
numbers the figure never lands on the page after the panels it numbers. A
|
|
169
|
+
group too tall to fit a page splits instead of failing the render, with the
|
|
170
|
+
panels still in source order.
|
|
171
|
+
|
|
172
|
+
`.columns-N` on the attribute line is honored when the page is wide enough to
|
|
173
|
+
give every column `figure.group.min_column_width` points, and is otherwise
|
|
174
|
+
ignored in favor of a stack. Every panel is drawn either way: the hint decides
|
|
175
|
+
arrangement, never content.
|
|
176
|
+
|
|
177
|
+
An opener that carries a title or a label (`::: figure "T"`, `::: figure [g]`)
|
|
178
|
+
is deliberately NOT this construct - it stays a generic container and renders
|
|
179
|
+
as one.
|
|
180
|
+
|
|
181
|
+
> [!NOTE]
|
|
182
|
+
> The parser this gem consumes (`carve-lang`, over carve-rs) does not produce
|
|
183
|
+
> `figure_group` nodes yet. The renderer accepts them today - through
|
|
184
|
+
> `Carve.parse` once the engine ships the construct, and through
|
|
185
|
+
> `Carve::Hexapdf.render_ast` with an AST from any engine that already does.
|
|
186
|
+
|
|
187
|
+
### Math and diagrams (renderer callables)
|
|
188
|
+
|
|
189
|
+
PDF has no client-side renderer, so math and diagram fences are turned into
|
|
190
|
+
embedded raster images through callables you supply in `renderers:`. Each
|
|
191
|
+
returns image bytes (PNG/JPG) as a String - or a Hash `{ bytes:, width:,
|
|
192
|
+
height: }` (points) to control the drawn size, so high-DPI rasters embed
|
|
193
|
+
crisply at their intended dimensions. A missing renderer, or one that returns
|
|
194
|
+
anything else or raises, degrades that construct to its monospace source.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
Carve::Hexapdf.render(source, renderers: {
|
|
198
|
+
# inline `$`x`$` and display `$$`x`$$` math:
|
|
199
|
+
math: ->(tex, display) { my_tex_to_png(tex, display) }, # -> bytes | {bytes:, width:, height:} | nil
|
|
200
|
+
# fenced ```mermaid / ```dot|graphviz / ```chart|vega:
|
|
201
|
+
mermaid: ->(src) { my_mermaid_to_png(src) },
|
|
202
|
+
graphviz: ->(src) { my_dot_to_png(src) },
|
|
203
|
+
chart: ->(src) { my_chart_to_png(src) },
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Graceful degradation
|
|
208
|
+
|
|
209
|
+
The renderer never raises on an unsupported node - it degrades so a document
|
|
210
|
+
always produces a PDF:
|
|
211
|
+
|
|
212
|
+
- **Math / diagram** fences without a matching `renderers:` callable render
|
|
213
|
+
their source in a monospace run.
|
|
214
|
+
- **Remote image URLs** (`http(s)://`) are shown as alt text - no network
|
|
215
|
+
fetching. Local files and `data:` URIs are embedded.
|
|
216
|
+
- **Raw HTML** blocks/inlines and **comments** are dropped.
|
|
217
|
+
|
|
218
|
+
## Licensing
|
|
219
|
+
|
|
220
|
+
This gem is **MIT** licensed. However, **HexaPDF is dual-licensed AGPL-3.0 /
|
|
221
|
+
commercial**. If you distribute software or offer it over a network while
|
|
222
|
+
depending on HexaPDF, you must comply with the AGPL (open-source your
|
|
223
|
+
application) or hold a
|
|
224
|
+
[HexaPDF commercial license](https://hexapdf.gettalong.org/#pricing). This gem
|
|
225
|
+
only bridges Carve to HexaPDF; your use of HexaPDF is governed by HexaPDF's own
|
|
226
|
+
terms.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT, markup-carve. See [LICENSE](LICENSE).
|