carve-lang 0.1.5 → 0.1.7
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 +78 -4
- data/README.md +62 -339
- data/ext/carve/Cargo.lock +5 -3
- data/ext/carve/Cargo.toml +2 -2
- data/ext/carve/src/lib.rs +215 -17
- data/lib/carve/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: e1129dd70ab09658be69bfbbf0158a02d599d9e814fb5e3f015be6d1a6cc5621
|
|
4
|
+
data.tar.gz: bc87753903ba603238e2c766b20c8384259dc8f44e87da90af4ef3f6d26ac613
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f4f5180ccfda64b312e035b7302db7de20b52293466dd1719715d7ab9fcdfa06ba61b23e993d042df746ced604219bdb78cd5931e09a6dd25388ee9fffcac102
|
|
7
|
+
data.tar.gz: c1276c1504398c7be637fc9e6699b42217e0a3845f49e7647348650701abdc25148fe2138c49643171154d8834c184ef711b294bd3f3215db29de509299e127c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.7] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The engine is the published `carve-lang` 0.1.8 crate, up from 0.1.7. It closes
|
|
15
|
+
every row `resources/spec-drift.txt` declared: the gem renders all 2225 corpus
|
|
16
|
+
documents spec main declares byte-identically, where 0.1.7 rendered 30 of them
|
|
17
|
+
by a superseded rule. The ledger is empty again, which is the only state a tag
|
|
18
|
+
may ship.
|
|
19
|
+
- **Breaking for a `Carve.to_html` consumer:** a heading cross-reference, a
|
|
20
|
+
numbered caption or equation reference, and a collapsed reference falling back
|
|
21
|
+
to heading text compare their target case exactly. `</#target>` against a
|
|
22
|
+
`# Target` heading no longer resolves and stays literal text; link-definition
|
|
23
|
+
labels, footnote labels and include fragment selectors already matched case
|
|
24
|
+
exactly. `carve fmt --migrate` in carve-rs repairs unambiguous case-only misses
|
|
25
|
+
(markup-carve/carve#2732, markup-carve/carve-rs#2320).
|
|
26
|
+
- **Breaking for a `Carve.to_html` consumer:** a glossary reference matches its
|
|
27
|
+
term exactly and links to the matched entry, and two terms differing only in
|
|
28
|
+
case take two ids (markup-carve/carve#2739).
|
|
29
|
+
- A render that blanks a denied destination scheme reports one
|
|
30
|
+
`destination-denied` loss, so a checked render of a `javascript:` destination
|
|
31
|
+
refuses where it used to pass. The emitted `href=""` does not move
|
|
32
|
+
(markup-carve/carve#2679).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `Carve::EnginePanic`, a `StandardError` subclass raised when the engine
|
|
37
|
+
panics, so a host rendering untrusted input can rescue one and keep serving.
|
|
38
|
+
The message carries the panic text and its location in the engine source, and
|
|
39
|
+
the usual panic report still reaches stderr (#170).
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- An engine panic arrives as a rescuable Ruby exception rather than `fatal`.
|
|
44
|
+
magnus catches the unwind but raises it as `fatal`, which no host can stop,
|
|
45
|
+
not even with `rescue Exception`, so any panic ended the process. The binding
|
|
46
|
+
now converts a caught panic itself (#170).
|
|
47
|
+
- A line holding a single `|` followed by an attribute block, such as `|{.r}`,
|
|
48
|
+
renders as paragraph text. Under the 0.1.7 engine it panicked inside the table
|
|
49
|
+
check, and a panic crossing the FFI boundary reached Ruby as `fatal`, which
|
|
50
|
+
`rescue` cannot catch, so five bytes of input terminated the host process
|
|
51
|
+
(markup-carve/carve-rs#2341).
|
|
52
|
+
|
|
53
|
+
## [0.1.6] - 2026-09-29
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- The engine is the published `carve-lang` 0.1.7 crate, up from 0.1.6. It closes
|
|
58
|
+
every row `resources/spec-drift.txt` declared: the gem renders all 2134 corpus
|
|
59
|
+
documents spec main declares byte-identically, where 0.1.6 rendered 158 of them
|
|
60
|
+
by a superseded rule. The ledger is empty again, which is the only state a tag
|
|
61
|
+
may ship (#151).
|
|
62
|
+
- **Breaking for a `Carve.parse` consumer:** a footnote reference node spells its
|
|
63
|
+
target as `label`, where it spelled it `id`. PART 12 section 25 settles that
|
|
64
|
+
name on the definition, and every node also carries `attrs[:id]` for an
|
|
65
|
+
authored `{#x}`, so the old name stood for two unrelated values on one object.
|
|
66
|
+
`attrs[:id]` is untouched (markup-carve/carve-rs#1853).
|
|
67
|
+
- **Breaking for a `Carve.parse` consumer:** a code block's `content` is the literal
|
|
68
|
+
payload text, so `a`, `a\n` and `a\n\n` stay distinct in the tree where they
|
|
69
|
+
collapsed to one shape, and an empty fence holds no newline. Rendered HTML is
|
|
70
|
+
unchanged (markup-carve/carve-rs#2191, markup-carve/carve-rs#2195).
|
|
71
|
+
- `Carve.parse` publishes twelve further fields the tree gained: a fenced
|
|
72
|
+
blockquote, a loose definition list, a directive's kind and children, a line
|
|
73
|
+
block's attributes, an authored task state, a lone-image paragraph, a table's
|
|
74
|
+
columns and row groups, and a cell's colspan, rowspan and vertical alignment.
|
|
75
|
+
- **Breaking for a `Carve.to_markdown` consumer:** the Markdown target follows
|
|
76
|
+
PART 11 section 11 for a GFM reader. A heading takes no `{#id}` suffix, and a
|
|
77
|
+
resolved cross-reference is written with the heading's GFM slug, so
|
|
78
|
+
`lowercase_heading_ids` no longer changes that anchor - the slug is the one a
|
|
79
|
+
GFM reader computes for itself. `Carve.to_html` still answers to the option
|
|
80
|
+
(markup-carve/carve-rs#2014).
|
|
81
|
+
|
|
10
82
|
## [0.1.5] - 2026-09-21
|
|
11
83
|
|
|
12
84
|
### Changed
|
|
@@ -287,7 +359,7 @@ are not listed, because no release ever shipped them.
|
|
|
287
359
|
Frontmatter and footnote definitions are block nodes in `children` rather
|
|
288
360
|
than root fields, which PART 12 §7 requires: a root field cannot carry the
|
|
289
361
|
position §4 requires of every node, and both are source an editor navigates
|
|
290
|
-
to (carve#411, carve#418). Frontmatter is the first child, carrying `format`
|
|
362
|
+
to (markup-carve/carve#411, markup-carve/carve#418). Frontmatter is the first child, carrying `format`
|
|
291
363
|
and **raw** `content` - not parsed key/values, which could not represent a
|
|
292
364
|
`---toml` block at all. Anything reading `ast[:frontmatter]` or
|
|
293
365
|
`ast[:footnoteDefs]` breaks.
|
|
@@ -296,9 +368,9 @@ are not listed, because no release ever shipped them.
|
|
|
296
368
|
backslash escape is `{"type":"escaped_text","value":"-"}` instead of being
|
|
297
369
|
folded into surrounding text - the backslash carries intent the character does
|
|
298
370
|
not, since an author writes `\-\-` precisely so a consumer will not render an
|
|
299
|
-
en dash (carve#350). A `::: |` fence is `line_block` instead of a `div` with a
|
|
371
|
+
en dash (markup-carve/carve#350). A `::: |` fence is `line_block` instead of a `div` with a
|
|
300
372
|
`.line-block` class, because inside it every newline is a hard break and a
|
|
301
|
-
class alone could not say which one a node was (carve#359). A block
|
|
373
|
+
class alone could not say which one a node was (markup-carve/carve#359). A block
|
|
302
374
|
extension's `summary` is a list of inline nodes rather than a plain string,
|
|
303
375
|
matching the admonition `title` shape. Definition lists publish
|
|
304
376
|
`definition_term` and `definition_description` nodes
|
|
@@ -372,7 +444,9 @@ are not listed, because no release ever shipped them.
|
|
|
372
444
|
Arrays (every AST node type is covered), enabling custom renderers such as
|
|
373
445
|
[carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
|
|
374
446
|
|
|
375
|
-
[Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.
|
|
447
|
+
[Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.7...HEAD
|
|
448
|
+
[0.1.7]: https://github.com/markup-carve/carve-rb/compare/v0.1.6...v0.1.7
|
|
449
|
+
[0.1.6]: https://github.com/markup-carve/carve-rb/compare/v0.1.5...v0.1.6
|
|
376
450
|
[0.1.5]: https://github.com/markup-carve/carve-rb/compare/v0.1.4...v0.1.5
|
|
377
451
|
[0.1.4]: https://github.com/markup-carve/carve-rb/compare/v0.1.3...v0.1.4
|
|
378
452
|
[0.1.3]: https://github.com/markup-carve/carve-rb/compare/v0.1.2...v0.1.3
|
data/README.md
CHANGED
|
@@ -1,387 +1,110 @@
|
|
|
1
1
|
# carve (Ruby)
|
|
2
2
|
|
|
3
|
-
Native Ruby bindings for the
|
|
4
|
-
markup language.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
is not reimplemented in Ruby; it calls into the Rust crate directly, mirroring
|
|
8
|
-
how Djot's `djotter` gem wraps the `jotdown` crate.
|
|
3
|
+
Native Ruby bindings for the
|
|
4
|
+
[Carve](https://github.com/markup-carve/carve) markup language. The gem wraps
|
|
5
|
+
the [carve-rs](https://github.com/markup-carve/carve-rs) engine through magnus
|
|
6
|
+
and rb-sys rather than reimplementing the parser.
|
|
9
7
|
|
|
10
8
|
## Install
|
|
11
9
|
|
|
12
10
|
```ruby
|
|
13
|
-
# Gemfile
|
|
14
11
|
gem "carve-lang"
|
|
15
12
|
```
|
|
16
13
|
|
|
17
|
-
```
|
|
14
|
+
```bash
|
|
18
15
|
bundle install
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
|
|
18
|
+
The distribution name is `carve-lang`, while the require path is `carve`.
|
|
19
|
+
Building from source requires Rust 1.75 or newer and Ruby development headers.
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
gem install carve-lang
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Then `require "carve"` as normal - the gem distribution name is `carve-lang`
|
|
28
|
-
but the require path stays `carve`.
|
|
29
|
-
|
|
30
|
-
Building from source requires a **Rust toolchain** (`cargo`, Rust >= 1.75) and
|
|
31
|
-
Ruby development headers. RubyGems compiles the native extension at install
|
|
32
|
-
time via `rb_sys`.
|
|
33
|
-
|
|
34
|
-
## Usage
|
|
21
|
+
## Render Carve
|
|
35
22
|
|
|
36
23
|
```ruby
|
|
37
24
|
require "carve"
|
|
38
25
|
|
|
39
26
|
Carve.to_html("# Hello *world*")
|
|
40
|
-
# => "<section id=\"Hello-world\">\n <h1>Hello <strong>world</strong></h1>\n</section>"
|
|
41
|
-
|
|
42
|
-
# Every core engine target is available from the binding.
|
|
43
27
|
Carve.to_markdown(source)
|
|
44
28
|
Carve.to_plain_text(source)
|
|
45
29
|
Carve.to_ansi(source)
|
|
46
30
|
Carve.to_carve(source)
|
|
47
|
-
# Import reports use schema version 2, with fidelity and confidence per finding.
|
|
48
|
-
# Markdown emits fidelity-unverified/dropped/fallback until its engine path
|
|
49
|
-
# exposes construct-level fidelity.
|
|
50
|
-
Carve.from_html('<p>Hello <strong>world</strong></p>')
|
|
51
|
-
Carve.from_markdown('*em* and **strong**')
|
|
52
|
-
|
|
53
|
-
# Carve syntax note: *...* is STRONG (bold), /.../ is EMPHASIS (italic).
|
|
54
|
-
Carve.to_html("*bold* and /italic/")
|
|
55
|
-
|
|
56
|
-
# Enable opt-in extensions (Symbols or Strings, snake_case or hyphenated):
|
|
57
|
-
Carve.to_html(<<~CRV, extensions: [:math_block])
|
|
58
|
-
```math
|
|
59
|
-
a^2 + b^2 = c^2
|
|
60
|
-
```
|
|
61
|
-
CRV
|
|
62
|
-
|
|
63
|
-
Carve.to_html(src, extensions: %w[math-block list-table])
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Recognized extensions
|
|
67
|
-
|
|
68
|
-
`Carve::EXTENSIONS` is the list, and it comes from the engine rather than from
|
|
69
|
-
a copy kept here that could fall behind it:
|
|
70
|
-
|
|
71
|
-
```ruby
|
|
72
|
-
Carve::EXTENSIONS
|
|
73
|
-
# => [:autolink, :citations, :"code-callouts", :"code-group", ...]
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
The canonical names are kebab-case (`:"math-block"`, `:"table-of-contents"`).
|
|
77
|
-
Snake_case spellings (`:math_block`) work as arguments, as do the short aliases
|
|
78
|
-
this binding has always taken: `:math`, `:permalinks`, `:mermaid`, `:dot`,
|
|
79
|
-
`:graphviz`, `:chart`, `:toc`.
|
|
80
|
-
|
|
81
|
-
An unknown extension name raises `ArgumentError`.
|
|
82
|
-
|
|
83
|
-
### File includes
|
|
84
|
-
|
|
85
|
-
Includes are opt-in and require an absolute containment root plus the absolute
|
|
86
|
-
path of the source document. The result carries the rendered value, warnings,
|
|
87
|
-
and root-relative dependencies. Other render methods leave include directives
|
|
88
|
-
literal.
|
|
89
|
-
|
|
90
|
-
```ruby
|
|
91
|
-
result = Carve.to_html_with_includes(
|
|
92
|
-
File.read("book.crv"),
|
|
93
|
-
root: File.expand_path("."),
|
|
94
|
-
source_path: File.expand_path("book.crv"),
|
|
95
|
-
max_depth: 16,
|
|
96
|
-
)
|
|
97
|
-
puts result[:value]
|
|
98
31
|
```
|
|
99
32
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
went through the include path: `extensions:`, `symbols:`, `profile:`, `mode:`,
|
|
103
|
-
`renderers:`, `safe:` and `sections:`.
|
|
104
|
-
|
|
105
|
-
`Carve.parse_with_includes` gives the expanded document as an AST instead, in
|
|
106
|
-
the shape `Carve.parse` returns, for a host that walks the tree rather than
|
|
107
|
-
rendering HTML:
|
|
33
|
+
Options configure extensions, safe rendering, profiles, symbols, section
|
|
34
|
+
wrappers, and custom renderers:
|
|
108
35
|
|
|
109
36
|
```ruby
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
37
|
+
Carve.to_html(
|
|
38
|
+
source,
|
|
39
|
+
extensions: %w[autolink list-table],
|
|
40
|
+
safe: true,
|
|
41
|
+
profile: "comment",
|
|
114
42
|
)
|
|
115
|
-
result[:value][:children]
|
|
116
43
|
```
|
|
117
44
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
an offset in a document the caller never passed.
|
|
121
|
-
|
|
122
|
-
`Carve.render_with_includes` takes the same arguments plus `target:`, one of
|
|
123
|
-
`"html"`, `"markdown"`, `"plain"`, `"ansi"` or `"ast"`. There is no `"carve"`
|
|
124
|
-
target: spec I15 excludes the Carve writer from expansion, because inlining a
|
|
125
|
-
child into the formatter's output rewrites the author's document rather than
|
|
126
|
-
formatting it.
|
|
127
|
-
|
|
128
|
-
The budgets `max_depth:`, `max_bytes:`, `max_resolver_calls:` and
|
|
129
|
-
`max_warnings:` pass through to the engine; omit one to keep its default. A
|
|
130
|
-
target refused by the byte budget still reports `resolved: true`, because
|
|
131
|
-
section 19 charges the budget for what the resolver handed back and a target is
|
|
132
|
-
resolved before its size is known.
|
|
133
|
-
|
|
134
|
-
## Parsing to an AST
|
|
135
|
-
|
|
136
|
-
`Carve.parse` returns the parsed document as a tree of Ruby Hashes and Arrays,
|
|
137
|
-
for consumers that want to walk or transform the document rather than render
|
|
138
|
-
HTML - for example a custom PDF renderer (see
|
|
139
|
-
[`carve-hexapdf`](https://github.com/markup-carve/carve-hexapdf)).
|
|
140
|
-
|
|
141
|
-
```ruby
|
|
142
|
-
Carve.parse("# Hello *world*")
|
|
143
|
-
# => {type: "document", frontmatter: {}, footnote_defs: {},
|
|
144
|
-
# children: [{type: "heading", level: 1,
|
|
145
|
-
# children: [{type: "text", value: "Hello "},
|
|
146
|
-
# {type: "emphasis", kind: "strong",
|
|
147
|
-
# children: [{type: "text", value: "world"}], attrs: nil}],
|
|
148
|
-
# attrs: nil}],
|
|
149
|
-
# source_len: 15}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Every node is a Hash with a `:type` key plus its fields; child collections are
|
|
153
|
-
Arrays; `:attrs` is `nil` or a Hash of `{id:, classes:, key_values:}`. Keys are
|
|
154
|
-
symbols. This is the raw parse tree (default profile, no extensions), so
|
|
155
|
-
render-stage extension rewrites are not applied.
|
|
156
|
-
|
|
157
|
-
## Static render mode + renderers
|
|
158
|
-
|
|
159
|
-
By default `Carve.to_html` renders **interactive** HTML: client-script
|
|
160
|
-
constructs (Mermaid/Graphviz/Chart diagrams, math) emit hydration elements
|
|
161
|
-
(`<pre class="mermaid">`, ...) and disclosure stays collapsed (`<details>`).
|
|
162
|
-
|
|
163
|
-
Pass `mode: :static` to emit **self-contained** HTML for print, PDF, or
|
|
164
|
-
archival. Static mode forces disclosure (`<details open>`) and pre-renders
|
|
165
|
-
client-script constructs through the `renderers:` callables you supply.
|
|
166
|
-
|
|
167
|
-
```ruby
|
|
168
|
-
Carve.to_html(<<~CRV, extensions: [:fenced_render], mode: :static,
|
|
169
|
-
renderers: { mermaid: ->(src) { "<svg>#{src}</svg>" } })
|
|
170
|
-
```mermaid
|
|
171
|
-
graph TD; A-->B
|
|
172
|
-
```
|
|
173
|
-
CRV
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### Renderer callable signatures
|
|
177
|
-
|
|
178
|
-
The `renderers:` Hash is keyed by Symbol or String (see
|
|
179
|
-
`Carve::RENDERER_KEYS`):
|
|
180
|
-
|
|
181
|
-
| Key | Callable signature | Receives |
|
|
182
|
-
| --- | ------------------ | -------- |
|
|
183
|
-
| `:mermaid` | `(String) -> String` | the diagram source |
|
|
184
|
-
| `:chart` | `(String) -> String` | the chart JSON source |
|
|
185
|
-
| `:graphviz` | `(String) -> String` | the DOT / Graphviz source |
|
|
186
|
-
| `:math` | `(String, display) -> String` | the TeX source and a `display` boolean (`true` for block / display math, `false` for inline) |
|
|
45
|
+
`Carve::EXTENSIONS` reports the names accepted by the bundled engine. Unknown
|
|
46
|
+
names raise `ArgumentError`.
|
|
187
47
|
|
|
188
|
-
|
|
189
|
-
diagram, MathML / HTML for math) that the engine emits **verbatim** on the
|
|
190
|
-
static path.
|
|
48
|
+
## Errors
|
|
191
49
|
|
|
192
|
-
|
|
50
|
+
An invalid argument raises `ArgumentError`, and a render the engine refuses -
|
|
51
|
+
input past a profile's `max_length`, or a denied construct - also raises
|
|
52
|
+
`ArgumentError` with the engine's reason.
|
|
193
53
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
(spec carve #205; siblings carve-js #242, carve-php #240, carve-rs #143,
|
|
200
|
-
carve-py #1).
|
|
201
|
-
|
|
202
|
-
An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
|
|
203
|
-
|
|
204
|
-
## Symbols
|
|
205
|
-
|
|
206
|
-
A `:name:` symbol renders its literal `:name:` source unless the name is in the
|
|
207
|
-
**symbols map** passed as `symbols:` (String or Symbol keys, String values):
|
|
208
|
-
|
|
209
|
-
```ruby
|
|
210
|
-
Carve.to_html("Ship it :rocket: :shrug:", symbols: { "rocket" => "🚀" })
|
|
211
|
-
# => "<p>Ship it 🚀 :shrug:</p>" (an unmapped name stays literal)
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
The leading word-boundary guard is unaffected by an active map: `a:b:c`,
|
|
215
|
-
`10:30:` and `me@example.com` never become symbols. A non-String value raises
|
|
216
|
-
`TypeError`.
|
|
217
|
-
|
|
218
|
-
> **Security: symbol values are TRUSTED RAW output.**
|
|
219
|
-
> A mapped value is inserted into the output **unescaped** - the same trust
|
|
220
|
-
> class as a `renderers:` callable. `{ "b" => "<b>x</b>" }` emits a real `<b>`
|
|
221
|
-
> element, not escaped text. This is deliberate (processor configuration is
|
|
222
|
-
> trusted). **Never build a symbols map out of untrusted / user-supplied
|
|
223
|
-
> input.**
|
|
224
|
-
|
|
225
|
-
## Section wrappers
|
|
226
|
-
|
|
227
|
-
A top-level heading is wrapped, along with the content following it up to the
|
|
228
|
-
next same-or-shallower heading, in a `<section>` carrying the heading's id (spec
|
|
229
|
-
PART 9 §13). Only the id moves - `{#install .featured}` gives
|
|
230
|
-
`<section id="install"><h2 class="featured">` - and a heading inside a
|
|
231
|
-
blockquote, div or list item is not wrapped at all.
|
|
232
|
-
|
|
233
|
-
Pass `sections: false` to render headings flat, with the id back on the `<h*>`:
|
|
234
|
-
|
|
235
|
-
```ruby
|
|
236
|
-
Carve.to_html("# A\n\np\n")
|
|
237
|
-
# => "<section id=\"A\">\n <h1>A</h1>\n <p>p</p>\n</section>"
|
|
238
|
-
|
|
239
|
-
Carve.to_html("# A\n\np\n", sections: false)
|
|
240
|
-
# => "<h1 id=\"A\">A</h1>\n<p>p</p>"
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
This is for a host whose CSS or JS assumes rendered blocks are direct children
|
|
244
|
-
of the content container - the `.stack > * + *` spacing idiom, `:first-child`,
|
|
245
|
-
`nth-child()` counting, DOM child walks - all of which stop matching once a
|
|
246
|
-
wrapper sits in between. It is the one output change that breaks a document
|
|
247
|
-
whose *source* migrated cleanly.
|
|
248
|
-
|
|
249
|
-
Nothing else changes: ids, collision dedup, `</#id>` cross-references, implicit
|
|
250
|
-
`[Heading][]` references and heading numbering all resolve against the slug
|
|
251
|
-
rather than the element carrying it. The endnotes
|
|
252
|
-
`<section role="doc-endnotes">` is a separate construct and is still emitted.
|
|
253
|
-
|
|
254
|
-
## Untrusted input
|
|
255
|
-
|
|
256
|
-
Carve's normative hardening is always on and needs no option: dangerous URL
|
|
257
|
-
schemes are blanked, event-handler attributes like `onclick` are dropped, and the
|
|
258
|
-
bidi override/isolate characters behind Trojan Source are removed from rendered
|
|
259
|
-
text.
|
|
260
|
-
|
|
261
|
-
Raw passthrough is the deliberate exception. A ` ```=html ` block or a
|
|
262
|
-
`` `…`{=html} `` span renders **verbatim** by design, so it is the one thing
|
|
263
|
-
input you did not author has to switch off:
|
|
54
|
+
If the engine panics, the call raises `Carve::EnginePanic`, a `StandardError`
|
|
55
|
+
subclass carrying the panic message and its location in the engine source. A
|
|
56
|
+
panic means the engine reached a state it believed impossible, so the document
|
|
57
|
+
is not renderable, but the process survives and a host can serve an error
|
|
58
|
+
instead of losing the worker:
|
|
264
59
|
|
|
265
60
|
``` ruby
|
|
266
|
-
|
|
61
|
+
begin
|
|
62
|
+
Carve.to_html(untrusted_source)
|
|
63
|
+
rescue Carve::EnginePanic => e
|
|
64
|
+
logger.error("carve engine panic: #{e.message}")
|
|
65
|
+
"<p>This document could not be rendered.</p>"
|
|
66
|
+
end
|
|
267
67
|
```
|
|
268
68
|
|
|
269
|
-
|
|
270
|
-
restricts which constructs are allowed at all and caps input length -
|
|
271
|
-
`:full`, `:article`, `:comment` or `:minimal`, String or Symbol. An unknown name
|
|
272
|
-
raises `ArgumentError` rather than being ignored.
|
|
273
|
-
|
|
274
|
-
A profile **rejection** raises too, rather than returning something that looks
|
|
275
|
-
like output:
|
|
69
|
+
Report any input that raises it: a panic is an engine defect, not a rejection.
|
|
276
70
|
|
|
277
|
-
|
|
278
|
-
Carve.to_html("x" * 20_000, profile: :minimal)
|
|
279
|
-
# ArgumentError: Profile violations: 'document' is not allowed: max_length_exceeded (...)
|
|
280
|
-
```
|
|
71
|
+
## Migration and AST access
|
|
281
72
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
73
|
+
`Carve.from_html` and `Carve.from_markdown` return canonical Carve with
|
|
74
|
+
structured fidelity reports. `Carve.parse` exposes a Ruby hash representation
|
|
75
|
+
of the AST, and static rendering produces self-contained output for documents
|
|
76
|
+
that do not load client-side renderers.
|
|
285
77
|
|
|
286
|
-
|
|
287
|
-
|
|
78
|
+
The [usage reference](docs/reference.md) documents AST fields, static
|
|
79
|
+
rendering, symbols, wrappers, profiles, and the main methods.
|
|
288
80
|
|
|
289
|
-
##
|
|
81
|
+
## Includes
|
|
290
82
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
`.crv` files can be checked for documents predating a breaking spec change:
|
|
83
|
+
Includes are opt-in and require an absolute containment root plus the source
|
|
84
|
+
document's absolute path:
|
|
294
85
|
|
|
295
|
-
```
|
|
296
|
-
Carve.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
An **unstamped** document answers `true`: its provenance is unknown, and assuming
|
|
303
|
-
it is current is the unsafe direction. Both marker forms are read, and a marker
|
|
304
|
-
written by any engine reads the same - the format is the contract, not any one
|
|
305
|
-
API - so the answer matches carve-php, carve-js, carve-rs and carve-go on the
|
|
306
|
-
same document.
|
|
307
|
-
|
|
308
|
-
What a version difference means is the
|
|
309
|
-
[versioning contract](https://markup-carve.github.io/carve/versioning): only
|
|
310
|
-
`[behavior]` changelog entries between the stamped version and yours can require
|
|
311
|
-
a document change.
|
|
312
|
-
|
|
313
|
-
## API
|
|
314
|
-
|
|
315
|
-
| Method | Description |
|
|
316
|
-
| ------ | ----------- |
|
|
317
|
-
| `Carve.to_html(source)` | Render Carve source to HTML. |
|
|
318
|
-
| `Carve.parse(source)` | Parse Carve source into an AST (tree of Ruby Hashes/Arrays). |
|
|
319
|
-
| `Carve.to_html(source, extensions: [...])` | Render with the named extensions enabled. |
|
|
320
|
-
| `Carve.to_html(source, mode: :static, renderers: {...})` | Render self-contained static HTML with build-time renderers. |
|
|
321
|
-
| `Carve.to_html(source, symbols: {...})` | Render with a `:name:` -> value symbol map (values are raw, see above). |
|
|
322
|
-
| `Carve.to_html(source, safe: true, profile: :comment)` | Render untrusted input: escape `=html` raw blocks/spans, restrict constructs. |
|
|
323
|
-
| `Carve.to_html(source, sections: false)` | Render headings flat, with the id on the `<h*>` instead of a `<section>` wrapper. |
|
|
324
|
-
| `Carve.to_html_with_includes(source, root:, source_path:)` | Render contained file includes and return warnings and dependencies. |
|
|
325
|
-
| `Carve.parse_with_includes(source, root:, source_path:)` | The same expansion, published as an AST instead of HTML. |
|
|
326
|
-
| `Carve.render_with_includes(source, root:, source_path:, target:)` | The expansion over any render target: html, markdown, plain, ansi or ast. |
|
|
327
|
-
| `Carve.read_stamp(source)` | Read a document's provenance marker: `{version:, generated_by:}` or `nil`. |
|
|
328
|
-
| `Carve.needs_review?(source)` | Whether a document predates this engine's spec version (unstamped counts as yes). |
|
|
329
|
-
| `Carve.to_html_with_extensions(source, names_array)` | Native primitive (Array of Strings). |
|
|
330
|
-
| `Carve.to_html_full(source, names_array, mode_string, renderers_hash)` | Native static-mode primitive. |
|
|
331
|
-
| `Carve.to_html_full_with_symbols(source, names_array, mode_string, renderers_hash, symbols_hash)` | Native primitive, static mode + symbol map. |
|
|
332
|
-
| `Carve::VERSION` | Gem version. |
|
|
333
|
-
| `Carve::EXTENSIONS` | Array of recognized extension symbols. |
|
|
334
|
-
| `Carve::MODES` | Array of recognized render modes (`:interactive`, `:static`). |
|
|
335
|
-
| `Carve::RENDERER_KEYS` | Array of recognized `renderers:` keys. |
|
|
336
|
-
|
|
337
|
-
## Develop
|
|
338
|
-
|
|
339
|
-
```sh
|
|
340
|
-
bundle install
|
|
341
|
-
rake compile # builds the Rust extension into lib/carve/carve.so
|
|
342
|
-
rake test # runs the minitest suite
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
> [!NOTE]
|
|
346
|
-
> The native build uses `rb_sys` + `bindgen` (libclang) to read Ruby's
|
|
347
|
-
> headers. On systems where libclang cannot find its builtin C headers (the
|
|
348
|
-
> `'stdarg.h' file not found` error), point it at the GCC builtin include dir:
|
|
349
|
-
>
|
|
350
|
-
> ```sh
|
|
351
|
-
> export BINDGEN_EXTRA_CLANG_ARGS="-I/usr/lib/gcc/x86_64-linux-gnu/13/include"
|
|
352
|
-
> ```
|
|
353
|
-
>
|
|
354
|
-
> (Adjust the GCC version directory to match your toolchain.)
|
|
355
|
-
|
|
356
|
-
## carve-rs dependency pin
|
|
357
|
-
|
|
358
|
-
`ext/carve/Cargo.toml` pins a specific carve-rs commit for reproducible gem
|
|
359
|
-
builds:
|
|
360
|
-
|
|
361
|
-
```toml
|
|
362
|
-
carve_rs = { package = "carve-lang", git = "https://github.com/markup-carve/carve-rs", rev = "..." }
|
|
86
|
+
```ruby
|
|
87
|
+
result = Carve.to_html_with_includes(
|
|
88
|
+
File.read("book.crv"),
|
|
89
|
+
root: File.expand_path("."),
|
|
90
|
+
source_path: File.expand_path("book.crv"),
|
|
91
|
+
)
|
|
363
92
|
```
|
|
364
93
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
reads as authoritative.
|
|
94
|
+
The result contains the value, warnings, and root-relative dependencies.
|
|
95
|
+
Other render methods leave directives literal. See the
|
|
96
|
+
[include reference](docs/reference.md#file-includes) for budgets, AST expansion,
|
|
97
|
+
targets, and containment behavior.
|
|
370
98
|
|
|
371
|
-
|
|
372
|
-
(carve-rs renamed it from `carve`), so a pin at any revision past that rename
|
|
373
|
-
needs `package = "carve-lang"` as above.
|
|
99
|
+
## Security
|
|
374
100
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
that was tested.
|
|
101
|
+
Use `safe: true` and a restrictive profile for untrusted documents. Custom
|
|
102
|
+
renderer callbacks and symbol values are trusted output and may emit raw HTML.
|
|
103
|
+
The [untrusted-input reference](docs/reference.md#untrusted-input) describes
|
|
104
|
+
resource limits and URL handling.
|
|
380
105
|
|
|
381
|
-
|
|
382
|
-
corpus through the compiled extension and requires byte-identical HTML, so a pin
|
|
383
|
-
that has fallen behind fails a build. Locally:
|
|
106
|
+
## Development
|
|
384
107
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
108
|
+
Source builds, tests, and the carve-rs dependency pin are documented under
|
|
109
|
+
[Development](docs/reference.md#develop) and
|
|
110
|
+
[carve-rs dependency pin](docs/reference.md#carve-rs-dependency-pin).
|
data/ext/carve/Cargo.lock
CHANGED
|
@@ -37,22 +37,24 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
|
|
37
37
|
|
|
38
38
|
[[package]]
|
|
39
39
|
name = "carve-lang"
|
|
40
|
-
version = "0.1.
|
|
40
|
+
version = "0.1.8"
|
|
41
41
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
42
|
-
checksum = "
|
|
42
|
+
checksum = "0889683244be278df2ff8e1cd88e9f932036a424bb8d838546636b7f11e93903"
|
|
43
43
|
dependencies = [
|
|
44
44
|
"html5ever",
|
|
45
45
|
"markup5ever_rcdom",
|
|
46
|
+
"memchr",
|
|
46
47
|
"pulldown-cmark",
|
|
47
48
|
"regex",
|
|
48
49
|
"serde",
|
|
49
50
|
"serde_json",
|
|
51
|
+
"smallvec",
|
|
50
52
|
"unicode-normalization",
|
|
51
53
|
]
|
|
52
54
|
|
|
53
55
|
[[package]]
|
|
54
56
|
name = "carve-rb"
|
|
55
|
-
version = "0.1.
|
|
57
|
+
version = "0.1.7"
|
|
56
58
|
dependencies = [
|
|
57
59
|
"carve-lang",
|
|
58
60
|
"magnus",
|
data/ext/carve/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "carve-rb"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
rust-version = "1.75"
|
|
6
6
|
publish = false
|
|
@@ -22,4 +22,4 @@ serde_json = "1"
|
|
|
22
22
|
# The Carve engine. Imported under the Rust crate alias `carve_rs` so the
|
|
23
23
|
# binding's own crate name (`carve-rb`) does not collide with the engine's
|
|
24
24
|
# package name (`carve`) in the Cargo lockfile.
|
|
25
|
-
carve_rs = { package = "carve-lang", version = "=0.1.
|
|
25
|
+
carve_rs = { package = "carve-lang", version = "=0.1.8" }
|
data/ext/carve/src/lib.rs
CHANGED
|
@@ -27,8 +27,12 @@
|
|
|
27
27
|
use carve_rs::extensions::registry;
|
|
28
28
|
use carve_rs::{CarveExtension, Mode, Options, Profile, StaticRenderers};
|
|
29
29
|
use magnus::value::{InnerValue, Opaque};
|
|
30
|
-
use magnus::{function, prelude::*, Error, RArray, RHash, Ruby, Value};
|
|
30
|
+
use magnus::{function, prelude::*, Error, ExceptionClass, RArray, RHash, Ruby, Value};
|
|
31
|
+
use std::any::Any;
|
|
32
|
+
use std::cell::RefCell;
|
|
33
|
+
use std::panic::{self, AssertUnwindSafe};
|
|
31
34
|
use std::path::{Path, PathBuf};
|
|
35
|
+
use std::sync::Once;
|
|
32
36
|
|
|
33
37
|
/// HTML-escape a string for the renderer-failure fallback path.
|
|
34
38
|
///
|
|
@@ -732,6 +736,192 @@ fn stamp_needs_review(source: String, current_version: Option<String>) -> bool {
|
|
|
732
736
|
carve_rs::needs_review(&source, ¤t)
|
|
733
737
|
}
|
|
734
738
|
|
|
739
|
+
/// Panic deliberately inside the extension, so a test can observe the FFI
|
|
740
|
+
/// panic contract rather than assuming it.
|
|
741
|
+
///
|
|
742
|
+
/// Nothing but a test calls this. It exists because no Carve input panics the
|
|
743
|
+
/// pinned engine: the one that did (`|{.r}`) is fixed, and a test that cannot
|
|
744
|
+
/// reach a panic cannot tell a working safety net from a missing one.
|
|
745
|
+
fn panic_probe() -> String {
|
|
746
|
+
panic!("deliberate panic from the Carve extension panic probe");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// FFI panic safety
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
//
|
|
753
|
+
// magnus wraps every exposed call in catch_unwind, but `Error::from_panic`
|
|
754
|
+
// raises the caught panic as Ruby's `fatal`, and Ruby does not let a host stop
|
|
755
|
+
// a `fatal` -- not with `rescue Exception`, not with anything. So the unwind
|
|
756
|
+
// that was supposed to protect the host still ends the process, and magnus
|
|
757
|
+
// offers no way to choose a different class.
|
|
758
|
+
//
|
|
759
|
+
// Everything Ruby can reach therefore runs inside `guard`, which catches the
|
|
760
|
+
// unwind FIRST and raises `Carve::EnginePanic` (a StandardError) instead. The
|
|
761
|
+
// `panic = "unwind"` compile flag is still what makes any of this possible,
|
|
762
|
+
// which is why `scripts/check-panic-unwind.sh` stays.
|
|
763
|
+
|
|
764
|
+
thread_local! {
|
|
765
|
+
/// Where the most recent panic on this thread came from.
|
|
766
|
+
///
|
|
767
|
+
/// The payload `catch_unwind` hands back carries the message but not the
|
|
768
|
+
/// location, and the location is the half that identifies the engine bug.
|
|
769
|
+
static PANIC_LOCATION: RefCell<Option<String>> = const { RefCell::new(None) };
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/// Record panic locations without taking over panic reporting.
|
|
773
|
+
///
|
|
774
|
+
/// The previous hook still runs, so the `thread '<unnamed>' panicked at ...`
|
|
775
|
+
/// line and any `RUST_BACKTRACE` output a developer relies on are unchanged.
|
|
776
|
+
fn install_panic_hook() {
|
|
777
|
+
static ONCE: Once = Once::new();
|
|
778
|
+
ONCE.call_once(|| {
|
|
779
|
+
let previous = panic::take_hook();
|
|
780
|
+
panic::set_hook(Box::new(move |info| {
|
|
781
|
+
let location = info
|
|
782
|
+
.location()
|
|
783
|
+
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()));
|
|
784
|
+
PANIC_LOCATION.with(|slot| *slot.borrow_mut() = location);
|
|
785
|
+
previous(info);
|
|
786
|
+
}));
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/// `Carve::EnginePanic`, or `RuntimeError` if the class cannot be looked up.
|
|
791
|
+
///
|
|
792
|
+
/// The fallback is still rescuable, which is the property that matters; losing
|
|
793
|
+
/// the specific class is better than losing the process.
|
|
794
|
+
fn engine_panic_class(ruby: &Ruby) -> ExceptionClass {
|
|
795
|
+
ruby.define_module("Carve")
|
|
796
|
+
.ok()
|
|
797
|
+
.and_then(|module| module.const_get::<_, ExceptionClass>("EnginePanic").ok())
|
|
798
|
+
.unwrap_or_else(|| ruby.exception_runtime_error())
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/// Turn a caught unwind payload into a rescuable Ruby exception.
|
|
802
|
+
fn engine_panic_error(payload: Box<dyn Any + Send>) -> Error {
|
|
803
|
+
let message = if let Some(m) = payload.downcast_ref::<&'static str>() {
|
|
804
|
+
(*m).to_string()
|
|
805
|
+
} else if let Some(m) = payload.downcast_ref::<String>() {
|
|
806
|
+
m.clone()
|
|
807
|
+
} else {
|
|
808
|
+
"panic".to_string()
|
|
809
|
+
};
|
|
810
|
+
let message = match PANIC_LOCATION.with(|slot| slot.borrow_mut().take()) {
|
|
811
|
+
Some(at) => format!("the Carve engine panicked at {at}: {message}"),
|
|
812
|
+
None => format!("the Carve engine panicked: {message}"),
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// Unchecked for the same reason magnus does it in `Error::from_panic`: this
|
|
816
|
+
// only runs while a Ruby thread is calling into the extension.
|
|
817
|
+
let ruby = unsafe { Ruby::get_unchecked() };
|
|
818
|
+
Error::new(engine_panic_class(&ruby), message)
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/// Run an exposed call with the panic net in front of magnus's.
|
|
822
|
+
fn guard<T>(f: impl FnOnce() -> Result<T, Error>) -> Result<T, Error> {
|
|
823
|
+
match panic::catch_unwind(AssertUnwindSafe(f)) {
|
|
824
|
+
Ok(result) => result,
|
|
825
|
+
Err(payload) => Err(engine_panic_error(payload)),
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/// Declare the guarded wrapper Ruby is given for an implementation function.
|
|
830
|
+
///
|
|
831
|
+
/// `=>` wraps an infallible implementation, `=>?` one that already returns
|
|
832
|
+
/// `Result`. Registering a bare implementation instead of its wrapper would
|
|
833
|
+
/// leave that one call raising `fatal` again, so `test/panic_unwind_test.rb`
|
|
834
|
+
/// reads the `function!` registrations below and fails on any name that is not
|
|
835
|
+
/// a `g_` wrapper.
|
|
836
|
+
macro_rules! guarded {
|
|
837
|
+
($wrapper:ident ( $($arg:ident : $ty:ty),* ) -> $ret:ty => $inner:ident) => {
|
|
838
|
+
#[allow(clippy::too_many_arguments)]
|
|
839
|
+
fn $wrapper($($arg: $ty),*) -> Result<$ret, Error> {
|
|
840
|
+
guard(|| Ok($inner($($arg),*)))
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
($wrapper:ident ( $($arg:ident : $ty:ty),* ) -> $ret:ty =>? $inner:ident) => {
|
|
844
|
+
#[allow(clippy::too_many_arguments)]
|
|
845
|
+
fn $wrapper($($arg: $ty),*) -> Result<$ret, Error> {
|
|
846
|
+
guard(|| $inner($($arg),*))
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
guarded!(g_to_html(source: String) -> String => to_html);
|
|
852
|
+
guarded!(g_to_markdown(source: String) -> String => to_markdown);
|
|
853
|
+
guarded!(g_to_plain_text(source: String) -> String => to_plain_text);
|
|
854
|
+
guarded!(g_to_ansi(source: String) -> String => to_ansi);
|
|
855
|
+
guarded!(g_to_carve(source: String) -> String => to_carve);
|
|
856
|
+
guarded!(g_from_markdown_json(source: String) -> String => from_markdown_json);
|
|
857
|
+
guarded!(g_to_ast_json(source: String) -> String => to_ast_json);
|
|
858
|
+
guarded!(g_extension_names() -> Vec<String> => extension_names);
|
|
859
|
+
guarded!(g_panic_probe() -> String => panic_probe);
|
|
860
|
+
guarded!(
|
|
861
|
+
g_stamp_needs_review(source: String, current_version: Option<String>) -> bool
|
|
862
|
+
=> stamp_needs_review
|
|
863
|
+
);
|
|
864
|
+
guarded!(
|
|
865
|
+
g_from_html_json(ruby: &Ruby, source: String, mode: String) -> String =>? from_html_json
|
|
866
|
+
);
|
|
867
|
+
guarded!(g_read_stamp(ruby: &Ruby, source: String) -> Value =>? read_stamp);
|
|
868
|
+
guarded!(
|
|
869
|
+
g_to_html_with_extensions(ruby: &Ruby, source: String, names: RArray) -> String
|
|
870
|
+
=>? to_html_with_extensions
|
|
871
|
+
);
|
|
872
|
+
guarded!(
|
|
873
|
+
g_to_html_full(
|
|
874
|
+
ruby: &Ruby,
|
|
875
|
+
source: String,
|
|
876
|
+
names: RArray,
|
|
877
|
+
mode: String,
|
|
878
|
+
renderers: RHash
|
|
879
|
+
) -> String =>? to_html_full
|
|
880
|
+
);
|
|
881
|
+
guarded!(
|
|
882
|
+
g_to_html_full_with_symbols(
|
|
883
|
+
ruby: &Ruby,
|
|
884
|
+
source: String,
|
|
885
|
+
names: RArray,
|
|
886
|
+
mode: String,
|
|
887
|
+
renderers: RHash,
|
|
888
|
+
symbols: RHash
|
|
889
|
+
) -> String =>? to_html_full_with_symbols
|
|
890
|
+
);
|
|
891
|
+
guarded!(
|
|
892
|
+
g_to_html_safe(
|
|
893
|
+
ruby: &Ruby,
|
|
894
|
+
source: String,
|
|
895
|
+
names: RArray,
|
|
896
|
+
mode: String,
|
|
897
|
+
renderers: RHash,
|
|
898
|
+
symbols: RHash,
|
|
899
|
+
safe: bool,
|
|
900
|
+
profile: Option<String>,
|
|
901
|
+
sections: bool
|
|
902
|
+
) -> String =>? to_html_safe
|
|
903
|
+
);
|
|
904
|
+
guarded!(
|
|
905
|
+
g_render_with_includes_json(
|
|
906
|
+
ruby: &Ruby,
|
|
907
|
+
source: String,
|
|
908
|
+
root: String,
|
|
909
|
+
source_path: String,
|
|
910
|
+
target: String,
|
|
911
|
+
names: RArray,
|
|
912
|
+
mode: String,
|
|
913
|
+
renderers: RHash,
|
|
914
|
+
symbols: RHash,
|
|
915
|
+
safe: bool,
|
|
916
|
+
profile: Option<String>,
|
|
917
|
+
sections: bool,
|
|
918
|
+
max_depth: Option<usize>,
|
|
919
|
+
max_bytes: Option<usize>,
|
|
920
|
+
max_resolver_calls: Option<usize>,
|
|
921
|
+
max_warnings: Option<usize>
|
|
922
|
+
) -> String =>? render_with_includes_json
|
|
923
|
+
);
|
|
924
|
+
|
|
735
925
|
/// Entry point invoked by Ruby when the extension is loaded.
|
|
736
926
|
///
|
|
737
927
|
/// `name = "carve"` makes the macro emit the `Init_carve` symbol that matches
|
|
@@ -739,35 +929,43 @@ fn stamp_needs_review(source: String, current_version: Option<String>) -> bool {
|
|
|
739
929
|
/// package is named `carve-rb`.
|
|
740
930
|
#[magnus::init(name = "carve")]
|
|
741
931
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
932
|
+
install_panic_hook();
|
|
933
|
+
|
|
742
934
|
let module = ruby.define_module("Carve")?;
|
|
935
|
+
// Raised when the engine panics. A StandardError subclass on purpose:
|
|
936
|
+
// magnus would otherwise surface the panic as `fatal`, which no host can
|
|
937
|
+
// rescue, so an embedder taking untrusted input had no defense at all
|
|
938
|
+
// (markup-carve/carve-rb#170).
|
|
939
|
+
module.define_error("EnginePanic", ruby.exception_standard_error())?;
|
|
743
940
|
// Native primitives. The pure-Ruby wrapper in lib/carve.rb defines the
|
|
744
941
|
// public `Carve.to_html(source, extensions:, mode:, renderers:)` on top of
|
|
745
942
|
// these. `_to_html` is the no-extension fast path; the wrapper owns the
|
|
746
943
|
// bare `to_html` name.
|
|
747
|
-
module.define_singleton_method("_to_html", function!(
|
|
748
|
-
module.define_singleton_method("to_markdown", function!(
|
|
749
|
-
module.define_singleton_method("to_plain_text", function!(
|
|
750
|
-
module.define_singleton_method("to_ansi", function!(
|
|
751
|
-
module.define_singleton_method("to_carve", function!(
|
|
944
|
+
module.define_singleton_method("_to_html", function!(g_to_html, 1))?;
|
|
945
|
+
module.define_singleton_method("to_markdown", function!(g_to_markdown, 1))?;
|
|
946
|
+
module.define_singleton_method("to_plain_text", function!(g_to_plain_text, 1))?;
|
|
947
|
+
module.define_singleton_method("to_ansi", function!(g_to_ansi, 1))?;
|
|
948
|
+
module.define_singleton_method("to_carve", function!(g_to_carve, 1))?;
|
|
752
949
|
module.define_singleton_method(
|
|
753
950
|
"_render_with_includes_json",
|
|
754
|
-
function!(
|
|
951
|
+
function!(g_render_with_includes_json, 15),
|
|
755
952
|
)?;
|
|
756
|
-
module.define_singleton_method("_from_html_json", function!(
|
|
757
|
-
module.define_singleton_method("_from_markdown_json", function!(
|
|
758
|
-
module.define_singleton_method("_to_ast_json", function!(
|
|
953
|
+
module.define_singleton_method("_from_html_json", function!(g_from_html_json, 2))?;
|
|
954
|
+
module.define_singleton_method("_from_markdown_json", function!(g_from_markdown_json, 1))?;
|
|
955
|
+
module.define_singleton_method("_to_ast_json", function!(g_to_ast_json, 1))?;
|
|
759
956
|
module.define_singleton_method(
|
|
760
957
|
"to_html_with_extensions",
|
|
761
|
-
function!(
|
|
958
|
+
function!(g_to_html_with_extensions, 2),
|
|
762
959
|
)?;
|
|
763
|
-
module.define_singleton_method("to_html_full", function!(
|
|
960
|
+
module.define_singleton_method("to_html_full", function!(g_to_html_full, 4))?;
|
|
764
961
|
module.define_singleton_method(
|
|
765
962
|
"to_html_full_with_symbols",
|
|
766
|
-
function!(
|
|
963
|
+
function!(g_to_html_full_with_symbols, 5),
|
|
767
964
|
)?;
|
|
768
|
-
module.define_singleton_method("_to_html_safe", function!(
|
|
769
|
-
module.define_singleton_method("_extension_names", function!(
|
|
770
|
-
module.define_singleton_method("_read_stamp", function!(
|
|
771
|
-
module.define_singleton_method("_stamp_needs_review", function!(
|
|
965
|
+
module.define_singleton_method("_to_html_safe", function!(g_to_html_safe, 8))?;
|
|
966
|
+
module.define_singleton_method("_extension_names", function!(g_extension_names, 0))?;
|
|
967
|
+
module.define_singleton_method("_read_stamp", function!(g_read_stamp, 1))?;
|
|
968
|
+
module.define_singleton_method("_stamp_needs_review", function!(g_stamp_needs_review, 2))?;
|
|
969
|
+
module.define_singleton_method("_panic_probe", function!(g_panic_probe, 0))?;
|
|
772
970
|
Ok(())
|
|
773
971
|
}
|
data/lib/carve/version.rb
CHANGED
|
@@ -8,5 +8,5 @@ module Carve
|
|
|
8
8
|
# compares it against ext/carve/Cargo.toml and against the newest cut
|
|
9
9
|
# CHANGELOG section on every run, and .github/workflows/release.yml refuses to
|
|
10
10
|
# publish a gem whose version is not the tag being released.
|
|
11
|
-
VERSION = "0.1.
|
|
11
|
+
VERSION = "0.1.7"
|
|
12
12
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: carve-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
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-10-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|