carve-lang 0.1.10-aarch64-linux
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 +541 -0
- data/LICENSE +21 -0
- data/README.md +154 -0
- data/lib/carve/3.0/carve.so +0 -0
- data/lib/carve/3.1/carve.so +0 -0
- data/lib/carve/3.2/carve.so +0 -0
- data/lib/carve/3.3/carve.so +0 -0
- data/lib/carve/3.4/carve.so +0 -0
- data/lib/carve/4.0/carve.so +0 -0
- data/lib/carve/version.rb +12 -0
- data/lib/carve.rb +385 -0
- metadata +102 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b214f8dbf9f5c494a35507e7636deb76e57e9731ea0e41576a4ed5fa15e32a82
|
|
4
|
+
data.tar.gz: b93ce900782e080a60f87bcc370490465d7746b3cdd1fb3898ff0d188159c0d0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4fd189dfc7372fef16f44d2c5d15cac5e88b78062d0e6ef7e4f620b2b4faa5b6b363fc0590d154c38972241c522e089f7cc731eed3275ce404f570825cc5191f
|
|
7
|
+
data.tar.gz: 9242e5b9e6dc0f76f06846f84e8df95163550d7d97a1a19bb737eaae1e87db840bf5b6981d20d404807c4fb332b90e8e24a10b993f4f79224631066787e4acf2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.10] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Breaking
|
|
13
|
+
|
|
14
|
+
- The engine is the published `carve-lang` 0.1.11 crate. A quote-prefixed `+`
|
|
15
|
+
needs one space after each `>`, so `>+` and `>>+` now render as text, and a
|
|
16
|
+
continuation marker reads only at column 0 or right after a block opener.
|
|
17
|
+
Re-check documents that place `+` elsewhere (#215, markup-carve/carve#3421).
|
|
18
|
+
- Includes of a file that is not valid UTF-8 are refused with an
|
|
19
|
+
`include-non-text` warning instead of being spliced (#215,
|
|
20
|
+
markup-carve/carve-rs#2885).
|
|
21
|
+
|
|
22
|
+
### Fixes
|
|
23
|
+
|
|
24
|
+
- The carve-lang 0.1.11 pin corrects container ownership for quotes, lazy lines,
|
|
25
|
+
colon divs and footnotes, fences inside nested lists and quotes, and list
|
|
26
|
+
tightness around captions (#215).
|
|
27
|
+
- Includes see through a byte order mark and keep link definitions and region
|
|
28
|
+
markers in place. Markdown import keeps link destinations, autolink brackets
|
|
29
|
+
and pipe escapes as written (#215).
|
|
30
|
+
- `Carve.render_with_includes` with the `markdown`, `plain` and `ansi` targets
|
|
31
|
+
keeps a link definition where it was written, as `Carve.to_markdown` does,
|
|
32
|
+
instead of moving it to the end (#221).
|
|
33
|
+
|
|
34
|
+
### Improvements
|
|
35
|
+
|
|
36
|
+
- Precompiled gems for `x86_64-linux`, `x86_64-linux-musl`, `aarch64-linux`,
|
|
37
|
+
`x86_64-darwin`, `arm64-darwin` and `x64-mingw-ucrt` install without a Rust
|
|
38
|
+
toolchain on Ruby 3.0 to 4.0 (#214).
|
|
39
|
+
- `Carve.to_carve(source, align_tables: true)` pads table columns to display
|
|
40
|
+
width (#215).
|
|
41
|
+
- `Carve.select_include_region(source, name, mode:)` resolves a named
|
|
42
|
+
`@region` to a Range of content lines and raises `Carve::IncludeRegionError`
|
|
43
|
+
for a missing or malformed region (#215).
|
|
44
|
+
- An include with `@code` or `@code:lang` splices a file or region as one code
|
|
45
|
+
block (#215).
|
|
46
|
+
- Include warnings carry `includedBy`, the chain of directives that reached the
|
|
47
|
+
file the warning arose in, with root-relative paths, lines and columns (#221).
|
|
48
|
+
|
|
49
|
+
## [0.1.9] - 2026-10-18
|
|
50
|
+
|
|
51
|
+
### Breaking
|
|
52
|
+
|
|
53
|
+
- `Carve.parse` and AST include results carry `astVersion: "1.2"` on the root.
|
|
54
|
+
Exact-key readers must accept the new field (#210).
|
|
55
|
+
- The carve-lang 0.1.10 pin adds `resolved` to resolved link and image references
|
|
56
|
+
and `form` to comments. `[text]()` and `![alt]()` render as links and images,
|
|
57
|
+
and a bare `[r]:` defines an empty destination. Escape `\[` to keep that
|
|
58
|
+
spelling literal. Checked render reports can also include
|
|
59
|
+
`table-span-flattened`, `table-header-row-demoted`,
|
|
60
|
+
`table-header-row-hoisted`, and `table-header-cell-demoted` (#207).
|
|
61
|
+
|
|
62
|
+
### Fixes
|
|
63
|
+
|
|
64
|
+
- Source strings in other encodings such as Shift_JIS are transcoded to UTF-8.
|
|
65
|
+
Malformed UTF-8 and binary bytes become replacement characters instead of
|
|
66
|
+
failing at the native boundary (#207).
|
|
67
|
+
|
|
68
|
+
### Improvements
|
|
69
|
+
|
|
70
|
+
- The carve-lang 0.1.10 pin adds CSV and TSV table fences and keeps plain-text
|
|
71
|
+
list continuation blocks at their item's indentation. It also corrects
|
|
72
|
+
parsing of nested lists with quotes and containers (#207).
|
|
73
|
+
|
|
74
|
+
## [0.1.8] - 2026-10-14
|
|
75
|
+
|
|
76
|
+
### Breaking
|
|
77
|
+
|
|
78
|
+
- The engine is the published `carve-lang` 0.1.9 crate, so the AST follows contract 1.1: a cell joined across lines holds a `cell_wrap` node between its text halves, tables carry `separatorWidths`, and `delim` and `bulletChar` appear only for `)` and `*` (#200, markup-carve/carve-rs#2482).
|
|
79
|
+
- New loss codes such as `carrier-attribute-dropped`, `list-style-dropped` and `frontmatter-synthesized`. A check that fails on unknown codes has to list them (#200, markup-carve/carve-rs#2523).
|
|
80
|
+
- Image alt text and quoted include paths decode punctuation escapes, so double a literal backslash. The include rename warning's rule id is now `include-id-rename` instead of `include-heading-id-rename` (#184, markup-carve/carve#2772).
|
|
81
|
+
|
|
82
|
+
### Fixes
|
|
83
|
+
|
|
84
|
+
- A Markdown writer refusal raises `ArgumentError` instead of panicking in the engine (#184).
|
|
85
|
+
- Adjacent alphabetic and Roman ordered lists stay separate through HTML import and formatting (#185).
|
|
86
|
+
- Under the article profile, denied raw blocks become code blocks that keep their payload (#185).
|
|
87
|
+
|
|
88
|
+
### Improvements
|
|
89
|
+
|
|
90
|
+
- Markdown import reports assess construct fidelity, including preserved constructs as well as losses (#185).
|
|
91
|
+
- Ordered lists keep a `)` delimiter as `data-delim` in HTML and through Markdown import (#185).
|
|
92
|
+
|
|
93
|
+
## [0.1.7] - 2026-10-07
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- The engine is the published `carve-lang` 0.1.8 crate, up from 0.1.7. It closes
|
|
98
|
+
every row `resources/spec-drift.txt` declared: the gem renders all 2225 corpus
|
|
99
|
+
documents spec main declares byte-identically, where 0.1.7 rendered 30 of them
|
|
100
|
+
by a superseded rule. The ledger is empty again, which is the only state a tag
|
|
101
|
+
may ship.
|
|
102
|
+
- **Breaking for a `Carve.to_html` consumer:** a heading cross-reference, a
|
|
103
|
+
numbered caption or equation reference, and a collapsed reference falling back
|
|
104
|
+
to heading text compare their target case exactly. `</#target>` against a
|
|
105
|
+
`# Target` heading no longer resolves and stays literal text; link-definition
|
|
106
|
+
labels, footnote labels and include fragment selectors already matched case
|
|
107
|
+
exactly. `carve fmt --migrate` in carve-rs repairs unambiguous case-only misses
|
|
108
|
+
(markup-carve/carve#2732, markup-carve/carve-rs#2320).
|
|
109
|
+
- **Breaking for a `Carve.to_html` consumer:** a glossary reference matches its
|
|
110
|
+
term exactly and links to the matched entry, and two terms differing only in
|
|
111
|
+
case take two ids (markup-carve/carve#2739).
|
|
112
|
+
- A render that blanks a denied destination scheme reports one
|
|
113
|
+
`destination-denied` loss, so a checked render of a `javascript:` destination
|
|
114
|
+
refuses where it used to pass. The emitted `href=""` does not move
|
|
115
|
+
(markup-carve/carve#2679).
|
|
116
|
+
|
|
117
|
+
### Added
|
|
118
|
+
|
|
119
|
+
- `Carve::EnginePanic`, a `StandardError` subclass raised when the engine
|
|
120
|
+
panics, so a host rendering untrusted input can rescue one and keep serving.
|
|
121
|
+
The message carries the panic text and its location in the engine source, and
|
|
122
|
+
the usual panic report still reaches stderr (#170).
|
|
123
|
+
|
|
124
|
+
### Fixed
|
|
125
|
+
|
|
126
|
+
- An engine panic arrives as a rescuable Ruby exception rather than `fatal`.
|
|
127
|
+
magnus catches the unwind but raises it as `fatal`, which no host can stop,
|
|
128
|
+
not even with `rescue Exception`, so any panic ended the process. The binding
|
|
129
|
+
now converts a caught panic itself (#170).
|
|
130
|
+
- A line holding a single `|` followed by an attribute block, such as `|{.r}`,
|
|
131
|
+
renders as paragraph text. Under the 0.1.7 engine it panicked inside the table
|
|
132
|
+
check, and a panic crossing the FFI boundary reached Ruby as `fatal`, which
|
|
133
|
+
`rescue` cannot catch, so five bytes of input terminated the host process
|
|
134
|
+
(markup-carve/carve-rs#2341).
|
|
135
|
+
|
|
136
|
+
## [0.1.6] - 2026-09-29
|
|
137
|
+
|
|
138
|
+
### Changed
|
|
139
|
+
|
|
140
|
+
- The engine is the published `carve-lang` 0.1.7 crate, up from 0.1.6. It closes
|
|
141
|
+
every row `resources/spec-drift.txt` declared: the gem renders all 2134 corpus
|
|
142
|
+
documents spec main declares byte-identically, where 0.1.6 rendered 158 of them
|
|
143
|
+
by a superseded rule. The ledger is empty again, which is the only state a tag
|
|
144
|
+
may ship (#151).
|
|
145
|
+
- **Breaking for a `Carve.parse` consumer:** a footnote reference node spells its
|
|
146
|
+
target as `label`, where it spelled it `id`. PART 12 section 25 settles that
|
|
147
|
+
name on the definition, and every node also carries `attrs[:id]` for an
|
|
148
|
+
authored `{#x}`, so the old name stood for two unrelated values on one object.
|
|
149
|
+
`attrs[:id]` is untouched (markup-carve/carve-rs#1853).
|
|
150
|
+
- **Breaking for a `Carve.parse` consumer:** a code block's `content` is the literal
|
|
151
|
+
payload text, so `a`, `a\n` and `a\n\n` stay distinct in the tree where they
|
|
152
|
+
collapsed to one shape, and an empty fence holds no newline. Rendered HTML is
|
|
153
|
+
unchanged (markup-carve/carve-rs#2191, markup-carve/carve-rs#2195).
|
|
154
|
+
- `Carve.parse` publishes twelve further fields the tree gained: a fenced
|
|
155
|
+
blockquote, a loose definition list, a directive's kind and children, a line
|
|
156
|
+
block's attributes, an authored task state, a lone-image paragraph, a table's
|
|
157
|
+
columns and row groups, and a cell's colspan, rowspan and vertical alignment.
|
|
158
|
+
- **Breaking for a `Carve.to_markdown` consumer:** the Markdown target follows
|
|
159
|
+
PART 11 section 11 for a GFM reader. A heading takes no `{#id}` suffix, and a
|
|
160
|
+
resolved cross-reference is written with the heading's GFM slug, so
|
|
161
|
+
`lowercase_heading_ids` no longer changes that anchor - the slug is the one a
|
|
162
|
+
GFM reader computes for itself. `Carve.to_html` still answers to the option
|
|
163
|
+
(markup-carve/carve-rs#2014).
|
|
164
|
+
|
|
165
|
+
## [0.1.5] - 2026-09-21
|
|
166
|
+
|
|
167
|
+
### Changed
|
|
168
|
+
|
|
169
|
+
- The engine comes from the published `carve-lang` 0.1.6 crate on crates.io
|
|
170
|
+
instead of a carve-rs git revision. `gem install carve-lang` no longer
|
|
171
|
+
fetches from GitHub while it compiles the extension. The engine code is the
|
|
172
|
+
same carve-rs 0.1.6 the previous release pinned by revision (#136).
|
|
173
|
+
|
|
174
|
+
## [0.1.4] - 2026-09-19
|
|
175
|
+
|
|
176
|
+
### Added
|
|
177
|
+
|
|
178
|
+
- `Carve.to_html_with_includes` renders a file-backed document with its
|
|
179
|
+
`{{ path }}` directives expanded, contained to an absolute root the caller
|
|
180
|
+
names. It returns the rendered HTML alongside sanitized warnings and
|
|
181
|
+
root-relative dependencies, and forwards extension and include budget
|
|
182
|
+
settings to the engine. The binding canonicalizes the root and the source
|
|
183
|
+
path and requires the source to sit inside the root (#123, #125).
|
|
184
|
+
- `Carve.parse_with_includes` publishes the expanded document as a tree, in the
|
|
185
|
+
shape `Carve.parse` returns, and the include path takes the same render
|
|
186
|
+
options the string entry points take. A consumer that draws from the tree
|
|
187
|
+
rather than from HTML, such as carve-hexapdf, was blocked without it. Its
|
|
188
|
+
nodes carry no `pos`: spec I4 leaves position remapping across an include out
|
|
189
|
+
of scope in every engine (#126, #127).
|
|
190
|
+
|
|
191
|
+
### Changed
|
|
192
|
+
|
|
193
|
+
- HTML and Markdown migration reports use schema version 2 with shared
|
|
194
|
+
preserved, normalized, degraded, and dropped fidelity plus confidence.
|
|
195
|
+
Markdown retains the existing `source_format` key as a compatibility alias
|
|
196
|
+
for the shared `sourceFormat` spelling and now emits a conservative
|
|
197
|
+
`fidelity-unverified` dropped/fallback finding on every import instead of
|
|
198
|
+
the previous empty diagnostics array (#117).
|
|
199
|
+
- **Breaking:** A substitution node in the tree carries `old` and `new` as
|
|
200
|
+
arrays of inline nodes, where it carried the strings `oldText` and `newText`.
|
|
201
|
+
A caller reading that node walks the halves instead of reading them (#130,
|
|
202
|
+
markup-carve/carve-rs#1756).
|
|
203
|
+
- The engine moves from carve-rs `42df4092` to released 0.1.6 (`d7837249`).
|
|
204
|
+
Beyond the substitution change, the range brings the include pass this
|
|
205
|
+
release exposes, mention and tag handling (a nameless mention or tag is
|
|
206
|
+
dropped and reported, an unspellable name is refused, attributes survive the
|
|
207
|
+
editor bridge), and writer and parser fixes: the Markdown and Carve writers
|
|
208
|
+
escape what would reopen a construct on the way back in, and parsing tightens
|
|
209
|
+
around braced inlines, forced closers, escaped markers, adjacent links, blank
|
|
210
|
+
table rows and a code span's closer. 1740 of 1740 corpus documents render
|
|
211
|
+
byte-identically, and `resources/spec-drift.txt` is empty.
|
|
212
|
+
|
|
213
|
+
## [0.1.3] - 2026-09-08
|
|
214
|
+
|
|
215
|
+
### Changed
|
|
216
|
+
|
|
217
|
+
- The engine moves from carve-rs `da45f9d2` to `42df4092`, 46 commits, clearing
|
|
218
|
+
the binding-parity drift the nightly run found on 34 corpus documents (#110).
|
|
219
|
+
A comment leaf span begins at its markup, an emptied definition description
|
|
220
|
+
narrows to its marker line, a footnote nested past a footnote body becomes its
|
|
221
|
+
own note, a nested item's lead fence or colon container takes its flush-left
|
|
222
|
+
body, and an all-blank or lone-pipe row is no longer read as a table (the lone
|
|
223
|
+
pipe no longer panics).
|
|
224
|
+
- The engine moves from carve-rs `2e9c43f2` to `da45f9d2`, 20 commits. A colon
|
|
225
|
+
followed by a space and a tab opens no description, an authored task state
|
|
226
|
+
reaches the tree and the HTML, a titled medium emits one title attribute, an
|
|
227
|
+
empty external link target is omitted, and a table continuation survives Djot
|
|
228
|
+
migration. The stale pin is what failed PART 12 binding parity in
|
|
229
|
+
markup-carve/carve#1877 - a binding has no vote of its own.
|
|
230
|
+
|
|
231
|
+
## [0.1.2] - 2026-08-27
|
|
232
|
+
|
|
233
|
+
### Security
|
|
234
|
+
|
|
235
|
+
- The gem is published through RubyGems trusted publishing rather than a
|
|
236
|
+
long-lived API key, and the gemspec sets `rubygems_mfa_required`, so RubyGems
|
|
237
|
+
refuses a push from an account without MFA (#81).
|
|
238
|
+
|
|
239
|
+
### Changed
|
|
240
|
+
|
|
241
|
+
- The engine moves from carve-rs `0.1.3` (`a33c42ad`), which `0.1.1` shipped, to
|
|
242
|
+
`bd19414d` - 106 commits across four pin bumps (#80, #82, #84, #87). Rendering
|
|
243
|
+
changes an existing document can see: a column's alignment defaults come from
|
|
244
|
+
the header section, an explicit alignment run is validated and inherited, and
|
|
245
|
+
a table cell's marker run must end at a space; tables gained semantic row
|
|
246
|
+
partitions and a list table can carry local headers; a braced hyphen pair is
|
|
247
|
+
an en dash, an empty brace pair is text, and the doubled run is the canonical
|
|
248
|
+
arrow; a hyphen run opening a word after whitespace is a flag, not a dash; a
|
|
249
|
+
continuation marker attaches only a flush-left block; the footnote backlink
|
|
250
|
+
has an accessible name; a caption no longer disables a diagram fence's preset;
|
|
251
|
+
the first code group is numbered like every other one; a heading at a content
|
|
252
|
+
column leaves no paragraph open, and a definition at a list item's content
|
|
253
|
+
column closes its paragraph; an all-blank raw payload stays distinct from an
|
|
254
|
+
absent one; and depth limits guard every recursive parse, render and AST-JSON
|
|
255
|
+
path. `<thead>` and `<tfoot>` also write one row per line, which changes the
|
|
256
|
+
emitted HTML without changing what it renders. The last bump adds: three blank
|
|
257
|
+
lines are a hard list boundary rather than a wide gap between two items, so a
|
|
258
|
+
document that used the run as spacing now parses as two lists; a heading id
|
|
259
|
+
can opt in to ASCII folding; a tab control renders as a button and a code
|
|
260
|
+
group takes the mode option; and a `%%` line a verbatim run has already
|
|
261
|
+
swallowed no longer produces a comment node after that run, which changes the
|
|
262
|
+
tree without changing the HTML (#84).
|
|
263
|
+
The fourth bump is 18 further commits, and over the spec corpus exactly one
|
|
264
|
+
document's HTML moves with it: a list marker at an item's content column
|
|
265
|
+
opens a sublist whether or not it is the item's first, so a marker that
|
|
266
|
+
follows a blank line and a paragraph inside an item no longer joins that
|
|
267
|
+
paragraph (markup-carve/carve-rs#1251). A further 104 documents change only
|
|
268
|
+
their reported positions, because a container's span now starts at its
|
|
269
|
+
opening markup and ends at its last placed child, and a definition list's
|
|
270
|
+
likewise (markup-carve/carve-rs#1238, markup-carve/carve-rs#1248,
|
|
271
|
+
markup-carve/carve-rs#1252). Nothing else an existing document can see
|
|
272
|
+
changes (#87).
|
|
273
|
+
- The engine then moves from `bd19414d` to `f2fbb24c` - 39 further commits
|
|
274
|
+
(#91). A container with no closer ends at its last placed child rather than
|
|
275
|
+
one codepoint past it, which is the PART 12 §4 extent defect this gem was
|
|
276
|
+
still the one implementation reporting (markup-carve/carve-rs#1308, #89); a
|
|
277
|
+
footnote continuation survives a blank run (markup-carve/carve-rs#1295); and
|
|
278
|
+
a list takes PART 9 §17 L7's consumed `loose` attribute, which loosens the
|
|
279
|
+
list instead of being emitted on it and which `Carve.parse` publishes on a
|
|
280
|
+
definition list as a `loose` field (markup-carve/carve-rs#1304). Seven corpus
|
|
281
|
+
documents move with the bump and all seven are documents the corpus gained,
|
|
282
|
+
so nothing that already rendered correctly changed.
|
|
283
|
+
- The embedded engine moves to carve-rs `42a952cb`. An indented lone image now
|
|
284
|
+
parses as a paragraph holding an inline image rather than a block image
|
|
285
|
+
(markup-carve/carve#1660, markup-carve/carve-rs#1347). AST only - the
|
|
286
|
+
rendered HTML is unchanged on every corpus document (#93).
|
|
287
|
+
- The embedded engine moves from `42a952cb` to `468e71af` - 70 further commits
|
|
288
|
+
across three pin bumps (#96, #97, #98), and each of the three changes what an
|
|
289
|
+
existing document renders as. The first two settle one rule between them: a
|
|
290
|
+
list item's content column is measured from the bare marker, so neither the
|
|
291
|
+
task checkbox nor a marker-attached attribute block widens it
|
|
292
|
+
(markup-carve/carve-rs#1364, markup-carve/carve-rs#1374,
|
|
293
|
+
markup-carve/carve-rs#1379). `-{#k} [x] a` has content column 2, so a `# h`
|
|
294
|
+
written under it at column 2 opens a heading inside the item where the wider
|
|
295
|
+
column made it lazy paragraph text. Also in those two: a lone image at an
|
|
296
|
+
item's content column keeps the item's looseness, restoring the `<p>` wrapper
|
|
297
|
+
the item used to drop (markup-carve/carve-rs#1359); the ANSI quote bar
|
|
298
|
+
reports containment, so a quoted heading, code block, table, thematic break
|
|
299
|
+
and lone image keep the bar and a quoted list's bar sits outside its marker
|
|
300
|
+
(markup-carve/carve-rs#1363); and on the import side an imported heading
|
|
301
|
+
records its id, an imported task item comes back a task item, an input's type
|
|
302
|
+
matches the checkbox keyword case-insensitively, an HTML comment imports as a
|
|
303
|
+
Carve comment, and an imported code block no longer gains a trailing blank
|
|
304
|
+
line on every pass (markup-carve/carve-rs#1361, markup-carve/carve-rs#1366,
|
|
305
|
+
markup-carve/carve-rs#1378, markup-carve/carve-rs#1386,
|
|
306
|
+
markup-carve/carve-rs#1384).
|
|
307
|
+
The third bump is 44 commits on its own, and rendered HTML moves on 50 corpus
|
|
308
|
+
documents with it. Measured against the corpus's own expected output at the
|
|
309
|
+
spec the new pin pins, the old pin differed on all 50 and the new pin on
|
|
310
|
+
none, so those 50 were being rendered wrongly and this is a correctness fix
|
|
311
|
+
rather than a preference. What an author notices is that a recognized block
|
|
312
|
+
opener written past a container's content column now opens that block inside
|
|
313
|
+
the container instead of folding into text: an indented `> q` under a list
|
|
314
|
+
item wrote `<p>> q</p>` and now writes a block quote, an indented `# h`
|
|
315
|
+
wrote `<p># h</p>` and now writes a heading, an indented code fence folded
|
|
316
|
+
into inline code inside a paragraph and now writes a `<pre>` block, and a
|
|
317
|
+
tab-indented `> quote` wrote text on the item's own line and now writes a
|
|
318
|
+
block quote; where a blank line preceded the block, the item is no longer
|
|
319
|
+
reported loose either. The same authored base applies inside a footnote body
|
|
320
|
+
and a definition description, so a table or a link reference definition
|
|
321
|
+
written past the body's column registers there (markup-carve/carve-rs#1387,
|
|
322
|
+
markup-carve/carve-rs#1420, markup-carve/carve-rs#1422,
|
|
323
|
+
markup-carve/carve-rs#1425, markup-carve/carve-rs#1431,
|
|
324
|
+
markup-carve/carve-rs#1432, markup-carve/carve-rs#1433, measured over the
|
|
325
|
+
corpus that markup-carve/carve-rs#1417 pins). Four further changes an
|
|
326
|
+
existing document can see ride along: an explicit id or class may begin with
|
|
327
|
+
an ASCII digit, so `::: 123` opens a div with that class instead of staying a
|
|
328
|
+
paragraph (markup-carve/carve-rs#1393); `::: >` spells a block quote with no
|
|
329
|
+
marker column, and its closing fence takes a caption
|
|
330
|
+
(markup-carve/carve-rs#1399, markup-carve/carve-rs#1411); reference and
|
|
331
|
+
footnote labels differing only in ASCII whitespace resolve to one definition
|
|
332
|
+
(markup-carve/carve-rs#1397); and `{align=left|right|center}` on an element
|
|
333
|
+
whose `align` means text alignment renders a CSS declaration rather than the
|
|
334
|
+
deprecated presentational attribute (markup-carve/carve-rs#1412).
|
|
335
|
+
`Carve.to_carve` also writes one space after a definition separator
|
|
336
|
+
(markup-carve/carve-rs#1426) (#98).
|
|
337
|
+
- The final release pin moves from `0c54d2ea` to the released carve-rs 0.1.4
|
|
338
|
+
(`2e9c43f2`). This closes every declared spec-drift window: link and
|
|
339
|
+
abbreviation definitions below a definition body's column follow the same
|
|
340
|
+
folding rule, a comment in a footnote body leaves no blank line behind, the
|
|
341
|
+
continuation marker's column gate reaches every container, an empty
|
|
342
|
+
description body writes the `{empty}` sentinel, citation items are typed and
|
|
343
|
+
positioned, and paragraph block-image promotion is published on the AST.
|
|
344
|
+
- The stale pin is what made this gem the only implementation failing PART 12
|
|
345
|
+
conformance in markup-carve/carve#1451 - a binding has no vote of its own, and
|
|
346
|
+
this one was voting with an engine 28 commits behind (#82). It recurred within
|
|
347
|
+
two days, on three documents and a pin one day old, because nothing in this
|
|
348
|
+
repository compares the tree this gem produces against the engine's own (#84).
|
|
349
|
+
It recurred again the next day, on one document, with that comparison still
|
|
350
|
+
unmerged (#86) and the pin 18 commits behind (#87). It recurred twice more
|
|
351
|
+
inside this window: at 39 commits behind, still carrying the PART 12 §4
|
|
352
|
+
extent defect (#89, #91), and again on a ruling only the tree can see,
|
|
353
|
+
which a byte-for-byte HTML comparison cannot detect (#93).
|
|
354
|
+
|
|
355
|
+
### Added
|
|
356
|
+
|
|
357
|
+
- `Carve.from_html(source, mode: :safe)` and `Carve.from_markdown(source)`
|
|
358
|
+
import an existing document into canonical Carve (#92). Both return the
|
|
359
|
+
shared migration shape `{value:, report:}`: `value` is the Carve source, and
|
|
360
|
+
`report` carries `diagnostics`, each with `code`, `message`, `severity` and,
|
|
361
|
+
where the engine placed one, `path`. `from_html` takes `:safe`, `:semantic`
|
|
362
|
+
or `:roundtrip` and reports back the `mode` and `adapter` it ran with; any
|
|
363
|
+
other mode raises `ArgumentError`. `from_markdown` reports
|
|
364
|
+
`source_format: "markdown"` and diagnoses nothing.
|
|
365
|
+
- `Carve.to_markdown`, `to_plain_text`, `to_ansi` and `to_carve`, so every core
|
|
366
|
+
render target the embedded engine already understood is reachable from Ruby
|
|
367
|
+
(#88). `Carve.to_html` and the AST entry points are unchanged.
|
|
368
|
+
|
|
369
|
+
## [0.1.1] - 2026-08-18
|
|
370
|
+
|
|
371
|
+
Everything below is the delta from `v0.1.0`, which is the only version a
|
|
372
|
+
reader can be upgrading from. The engine pin moved seven times inside this
|
|
373
|
+
window and lands on carve-rs `0.1.3` (`a33c42ad`); the intermediate revisions
|
|
374
|
+
are not listed, because no release ever shipped them.
|
|
375
|
+
|
|
376
|
+
### Security
|
|
377
|
+
|
|
378
|
+
- **A list-valued URL attribute is probed at every candidate, not at its head**
|
|
379
|
+
(PART 9 §25, markup-carve/carve#1320). The sanitizer read only the value's
|
|
380
|
+
leading scheme, which vouches for the whole value only where the whole value
|
|
381
|
+
is one URL, so `srcset="safe.png 1x, javascript:alert(1) 2x"` passed on its
|
|
382
|
+
second entry. `srcset`, `imagesrcset`, `ping` and `attributionsrc` are now
|
|
383
|
+
split into tokens and every candidate is read, and any hit blanks the whole
|
|
384
|
+
value. The engine embedded in `0.1.0` predates the fix, so the gem published
|
|
385
|
+
on 2026-07-12 carries the defect.
|
|
386
|
+
|
|
387
|
+
### Added
|
|
388
|
+
|
|
389
|
+
- **`Carve.parse` publishes source positions.** Every block node the engine can
|
|
390
|
+
place carries `pos` with `startLine`, `endLine`, `startColumn`, `endColumn`,
|
|
391
|
+
`startOffset` and `endOffset`, as PART 12 §4 spells it: lines and columns
|
|
392
|
+
1-based, offsets 0-based, counted in Unicode codepoints, `endColumn` and
|
|
393
|
+
`endOffset` exclusive. A node whose span the engine could not determine has
|
|
394
|
+
no `pos` key at all rather than one holding placeholder numbers - an absent
|
|
395
|
+
position is a fact a consumer can act on, an invented one is not. Inline
|
|
396
|
+
nodes do not carry positions yet (markup-carve/carve-rs#333).
|
|
397
|
+
|
|
398
|
+
- **`Carve.to_html(source, sections: false)` renders headings without the
|
|
399
|
+
`<section>` wrapper** (#36, PART 9 §13). The id goes back on the `<h*>`
|
|
400
|
+
alongside its other attributes and the blocks that would have been section
|
|
401
|
+
children stay siblings. Default `true`, so existing output is unchanged. The
|
|
402
|
+
wrapper is the one output change that breaks a site whose source migrated
|
|
403
|
+
cleanly: CSS and JS assuming rendered blocks are direct children of the
|
|
404
|
+
content container stop matching once a `<section>` sits in between.
|
|
405
|
+
|
|
406
|
+
- **Every extension the engine registers is reachable from Ruby.**
|
|
407
|
+
`Carve::EXTENSIONS` lists 31 names instead of 15, adding glossary, index,
|
|
408
|
+
table-of-contents placement (`::: toc`), heading numbers, heading references,
|
|
409
|
+
heading level shift, code groups, tabs, the img fence, color swatches, smart
|
|
410
|
+
quotes, and the remaining fenced-render presets (PlantUML, D2, WaveDrom,
|
|
411
|
+
Vega-Lite, ABC). The list comes from the engine's registry rather than being
|
|
412
|
+
typed out in the native binding and again in `lib/carve.rb`. Canonical names
|
|
413
|
+
are the engine's kebab-case keys; snake_case spellings and the short aliases
|
|
414
|
+
(`:math`, `:permalinks`, `:mermaid`, `:dot`, `:chart`, `:toc`) still work.
|
|
415
|
+
|
|
416
|
+
- **Composite figures** (PART 9 §4c). A **bare** `::: figure` fence is no longer
|
|
417
|
+
a generic container: it is one figure of ordered panels, rendering
|
|
418
|
+
`<figure class="carve-figure-group">` around a
|
|
419
|
+
`<div class="carve-figure-panels">` whose members are each a
|
|
420
|
+
`<figure class="carve-figure-panel">`, and a `^ ` line following the closer
|
|
421
|
+
becomes the group's `<figcaption>` instead of an ordinary paragraph. A fence
|
|
422
|
+
carrying a title or a `[label]` keeps the old shape, so a document that named
|
|
423
|
+
its figure divs renders as before. `Carve.parse` publishes the group as a new
|
|
424
|
+
`figure_group` node type.
|
|
425
|
+
|
|
426
|
+
### Changed
|
|
427
|
+
|
|
428
|
+
- **Breaking (`Carve.parse`): the published tree now matches the reference
|
|
429
|
+
shape.** A tree from this binding did not interoperate with carve-js or
|
|
430
|
+
carve-php, so several names and the root's shape moved at once:
|
|
431
|
+
|
|
432
|
+
| was | now |
|
|
433
|
+
|---|---|
|
|
434
|
+
| `critic_insert` / `critic_delete` / `critic_substitute` | `insert` / `delete` / `substitution` |
|
|
435
|
+
| `footnote` (both forms) | `footnote_ref` / `inline_footnote` |
|
|
436
|
+
| `ref_id` | `refId` |
|
|
437
|
+
| `footnote_defs` | `footnoteDefs` |
|
|
438
|
+
| `source_len` | `srcByteLength` |
|
|
439
|
+
| `emoji` | `symbol` (and it carries `attrs`) |
|
|
440
|
+
|
|
441
|
+
The root now carries exactly `type`, `children` and `srcByteLength`.
|
|
442
|
+
Frontmatter and footnote definitions are block nodes in `children` rather
|
|
443
|
+
than root fields, which PART 12 §7 requires: a root field cannot carry the
|
|
444
|
+
position §4 requires of every node, and both are source an editor navigates
|
|
445
|
+
to (markup-carve/carve#411, markup-carve/carve#418). Frontmatter is the first child, carrying `format`
|
|
446
|
+
and **raw** `content` - not parsed key/values, which could not represent a
|
|
447
|
+
`---toml` block at all. Anything reading `ast[:frontmatter]` or
|
|
448
|
+
`ast[:footnoteDefs]` breaks.
|
|
449
|
+
|
|
450
|
+
- **Breaking (`Carve.parse`): three constructs became their own node types.** A
|
|
451
|
+
backslash escape is `{"type":"escaped_text","value":"-"}` instead of being
|
|
452
|
+
folded into surrounding text - the backslash carries intent the character does
|
|
453
|
+
not, since an author writes `\-\-` precisely so a consumer will not render an
|
|
454
|
+
en dash (markup-carve/carve#350). A `::: |` fence is `line_block` instead of a `div` with a
|
|
455
|
+
`.line-block` class, because inside it every newline is a hard break and a
|
|
456
|
+
class alone could not say which one a node was (markup-carve/carve#359). A block
|
|
457
|
+
extension's `summary` is a list of inline nodes rather than a plain string,
|
|
458
|
+
matching the admonition `title` shape. Definition lists publish
|
|
459
|
+
`definition_term` and `definition_description` nodes
|
|
460
|
+
(markup-carve/carve-rs#374).
|
|
461
|
+
|
|
462
|
+
These are additive for a consumer with a default branch and a compile error
|
|
463
|
+
for one without.
|
|
464
|
+
|
|
465
|
+
- **Delimited inline comments carried in from the engine.** `{% … %}` is an
|
|
466
|
+
inline comment that ends at its closer, alongside the trailing `%%` form that
|
|
467
|
+
runs to the end of its inline run (PART 9 §21a). Both render nothing; in
|
|
468
|
+
`Carve.parse` they are the same `comment` node, told apart by a `delimited`
|
|
469
|
+
field, because the two spell different documents and a writer has to
|
|
470
|
+
reproduce the one that was written.
|
|
471
|
+
|
|
472
|
+
- **Language changes carried in from the engine.** Superscript and subscript are
|
|
473
|
+
braced-only - bare `^x^` and `,x,` are literal text, the markup is `{^x^}` and
|
|
474
|
+
`{,x,}`. The `:name:` symbol inline was added, with a leading word-boundary
|
|
475
|
+
guard so `a:b:c`, `10:30:` and `me@example.com` stay literal. Compact semantic
|
|
476
|
+
spans arrived: `[Tab]{kbd}` renders `<kbd>Tab</kbd>`, with `abbr`, `time` and
|
|
477
|
+
`kbd` in core and `samp`, `var`, `cite` and `dfn` behind the `semantic-span`
|
|
478
|
+
extension.
|
|
479
|
+
|
|
480
|
+
Every `<th>` carries a `scope` - `col` in the header run, `row` for a
|
|
481
|
+
`|=` cell in a body row. A cell's attribute block binds after its kind and
|
|
482
|
+
alignment markers, so a `{...}` following a cell's `=` marker is read as
|
|
483
|
+
attributes rather than rendered as cell text. A mandatory base class merges
|
|
484
|
+
into the author's class slot at its authored position rather than leading it,
|
|
485
|
+
so `:widget[x]{#i .c}` keeps `id` first.
|
|
486
|
+
|
|
487
|
+
- **Rendering corrections carried in from the engine.** A block-attribute line
|
|
488
|
+
reaches the nested list it was written for, and a flush-left attribute line is
|
|
489
|
+
read as attributes rather than as paragraph text. A figure group holds its
|
|
490
|
+
panels directly. A table's sections and rows keep the attributes they have a
|
|
491
|
+
slot for. A code block resolves the no-break-space sentinel instead of
|
|
492
|
+
emitting it. A fence opened inside a
|
|
493
|
+
container keeps that container open, so a boundary line, a list marker at the
|
|
494
|
+
content column and a closed fence's residue land where PART 9 §24 puts them
|
|
495
|
+
rather than folding into the code text. A lazy line folded into a container
|
|
496
|
+
leaves it open. A caption attaches across at most one blank line, so two blank
|
|
497
|
+
lines detach it and the image stays an image. A definition body is an
|
|
498
|
+
indented-block collector, so a line below its column ends it. A heading with
|
|
499
|
+
no `<section>` wrapper renders the author's attributes first and the generated
|
|
500
|
+
id last. A frontmatter block whose opener named no format is written back as
|
|
501
|
+
`---yaml`, and a blank line inside a fenced block under a footnote definition
|
|
502
|
+
or a definition-list description is written empty rather than indented.
|
|
503
|
+
|
|
504
|
+
Reference resolution reaches three places it used to stop short of: a
|
|
505
|
+
reference inside an inline note, a critic insertion or a critic deletion
|
|
506
|
+
resolves against the document's definitions; a footnote inside an unresolved
|
|
507
|
+
reference stays a footnote rather than being swallowed by the failed
|
|
508
|
+
reference; and a reference tail no longer seals its own link text, so the
|
|
509
|
+
text a reference link carries survives the frame that resolves it.
|
|
510
|
+
|
|
511
|
+
For `Carve.parse` specifically: a nested link and an autolink stay nodes and
|
|
512
|
+
the renderers unwrap them; a collapsed reference publishes the label it
|
|
513
|
+
resolves by; a heading's derived display text clones the heading's nodes
|
|
514
|
+
instead of re-rendering them, so an escaped character in a heading reaches the
|
|
515
|
+
label; and `attrs.keyValues` is published in the author's source order, the
|
|
516
|
+
same order the sibling `attrs.order` field states.
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
## [0.1.0] - 2026-07-12
|
|
520
|
+
|
|
521
|
+
### Added
|
|
522
|
+
|
|
523
|
+
- Initial release: `Carve.to_html` exposes the carve-rs engine as a native
|
|
524
|
+
Ruby extension (magnus + rb-sys), with the full extension/option surface,
|
|
525
|
+
the static/print render mode, and pluggable diagram/math renderers.
|
|
526
|
+
- `Carve.parse` returns the full parse tree as symbol-keyed Ruby Hashes and
|
|
527
|
+
Arrays (every AST node type is covered), enabling custom renderers such as
|
|
528
|
+
[carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
|
|
529
|
+
|
|
530
|
+
[Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.10...HEAD
|
|
531
|
+
[0.1.10]: https://github.com/markup-carve/carve-rb/compare/v0.1.9...v0.1.10
|
|
532
|
+
[0.1.9]: https://github.com/markup-carve/carve-rb/compare/v0.1.8...v0.1.9
|
|
533
|
+
[0.1.8]: https://github.com/markup-carve/carve-rb/compare/v0.1.7...v0.1.8
|
|
534
|
+
[0.1.7]: https://github.com/markup-carve/carve-rb/compare/v0.1.6...v0.1.7
|
|
535
|
+
[0.1.6]: https://github.com/markup-carve/carve-rb/compare/v0.1.5...v0.1.6
|
|
536
|
+
[0.1.5]: https://github.com/markup-carve/carve-rb/compare/v0.1.4...v0.1.5
|
|
537
|
+
[0.1.4]: https://github.com/markup-carve/carve-rb/compare/v0.1.3...v0.1.4
|
|
538
|
+
[0.1.3]: https://github.com/markup-carve/carve-rb/compare/v0.1.2...v0.1.3
|
|
539
|
+
[0.1.2]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...v0.1.2
|
|
540
|
+
[0.1.1]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...v0.1.1
|
|
541
|
+
[0.1.0]: https://github.com/markup-carve/carve-rb/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 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,154 @@
|
|
|
1
|
+
# carve (Ruby)
|
|
2
|
+
|
|
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.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "carve-lang"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bundle install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The distribution name is `carve-lang`, while the require path is `carve`.
|
|
19
|
+
|
|
20
|
+
Releases after 0.1.9 ship precompiled gems for `x86_64-linux`,
|
|
21
|
+
`x86_64-linux-musl`, `aarch64-linux`, `x86_64-darwin`, `arm64-darwin` and
|
|
22
|
+
`x64-mingw-ucrt`, with a binary for each Ruby from 3.0 to 4.0 (from 3.1 on
|
|
23
|
+
Windows, where Ruby 3.0 has no UCRT build). On those platforms `gem install`
|
|
24
|
+
needs no Rust toolchain or libclang. Everywhere else, including newer Rubies,
|
|
25
|
+
RubyGems installs the source gem and builds it as described below.
|
|
26
|
+
|
|
27
|
+
## Installing from source
|
|
28
|
+
|
|
29
|
+
Version 0.1.9 and earlier ship source only, and so does every release on a
|
|
30
|
+
platform or Ruby without a precompiled gem. There `gem install carve-lang`
|
|
31
|
+
compiles a Rust extension on your machine, so the install needs:
|
|
32
|
+
|
|
33
|
+
- A Rust toolchain with `cargo`, Rust 1.77 or newer. [rustup](https://rustup.rs)
|
|
34
|
+
is the usual way to get one.
|
|
35
|
+
- Ruby development headers (`ruby-dev` on Debian and Ubuntu).
|
|
36
|
+
- libclang and clang. The build runs `bindgen` through `rb-sys` to read Ruby's
|
|
37
|
+
C headers. On Debian and Ubuntu:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sudo apt install libclang-dev clang
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If only libclang is installed, without the `clang` package, the build stops
|
|
44
|
+
with this error:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/usr/include/ruby-3.2.0/ruby/ruby.h:23:10: fatal error: 'stdarg.h' file not found
|
|
48
|
+
thread 'main' panicked at .../rb-sys-0.9.128/build/main.rs:52:6:
|
|
49
|
+
generate bindings: ClangDiagnostic("/usr/include/ruby-3.2.0/ruby/ruby.h:23:10: fatal error: 'stdarg.h' file not found\n")
|
|
50
|
+
error: failed to run custom build command for `rb-sys v0.9.128`
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
libclang cannot find its own builtin headers. Install `clang`, or point
|
|
54
|
+
bindgen at GCC's copy of those headers:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
BINDGEN_EXTRA_CLANG_ARGS="-I$(gcc -print-file-name=include)" gem install carve-lang
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
With Bundler, export the variable before running `bundle install`.
|
|
61
|
+
|
|
62
|
+
## Render Carve
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
require "carve"
|
|
66
|
+
|
|
67
|
+
Carve.to_html("# Hello *world*")
|
|
68
|
+
Carve.to_markdown(source)
|
|
69
|
+
Carve.to_plain_text(source)
|
|
70
|
+
Carve.to_ansi(source)
|
|
71
|
+
Carve.to_carve(source)
|
|
72
|
+
Carve.to_carve(source, align_tables: true) # pad table columns
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Options configure extensions, safe rendering, profiles, symbols, section
|
|
76
|
+
wrappers, and custom renderers:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
Carve.to_html(
|
|
80
|
+
source,
|
|
81
|
+
extensions: %w[autolink list-table],
|
|
82
|
+
safe: true,
|
|
83
|
+
profile: "comment",
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`Carve::EXTENSIONS` reports the names accepted by the bundled engine. Unknown
|
|
88
|
+
names raise `ArgumentError`.
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
An invalid argument raises `ArgumentError`, and a render the engine refuses -
|
|
93
|
+
input past a profile's `max_length`, or a denied construct - also raises
|
|
94
|
+
`ArgumentError` with the engine's reason.
|
|
95
|
+
|
|
96
|
+
If the engine panics, the call raises `Carve::EnginePanic`, a `StandardError`
|
|
97
|
+
subclass carrying the panic message and its location in the engine source. A
|
|
98
|
+
panic means the engine reached a state it believed impossible, so the document
|
|
99
|
+
is not renderable, but the process survives and a host can serve an error
|
|
100
|
+
instead of losing the worker:
|
|
101
|
+
|
|
102
|
+
``` ruby
|
|
103
|
+
begin
|
|
104
|
+
Carve.to_html(untrusted_source)
|
|
105
|
+
rescue Carve::EnginePanic => e
|
|
106
|
+
logger.error("carve engine panic: #{e.message}")
|
|
107
|
+
"<p>This document could not be rendered.</p>"
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Report any input that raises it: a panic is an engine defect, not a rejection.
|
|
112
|
+
|
|
113
|
+
## Migration and AST access
|
|
114
|
+
|
|
115
|
+
`Carve.from_html` and `Carve.from_markdown` return canonical Carve with
|
|
116
|
+
structured fidelity reports. `Carve.parse` exposes a Ruby hash representation
|
|
117
|
+
of the AST, and static rendering produces self-contained output for documents
|
|
118
|
+
that do not load client-side renderers.
|
|
119
|
+
|
|
120
|
+
The [usage reference](docs/reference.md) documents AST fields, static
|
|
121
|
+
rendering, symbols, wrappers, profiles, and the main methods.
|
|
122
|
+
|
|
123
|
+
## Includes
|
|
124
|
+
|
|
125
|
+
Includes are opt-in and require an absolute containment root plus the source
|
|
126
|
+
document's absolute path:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
result = Carve.to_html_with_includes(
|
|
130
|
+
File.read("book.crv"),
|
|
131
|
+
root: File.expand_path("."),
|
|
132
|
+
source_path: File.expand_path("book.crv"),
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The result contains the value, warnings, and root-relative dependencies. A
|
|
137
|
+
warning from an included file names the chain of directives that reached it in
|
|
138
|
+
`:includedBy`.
|
|
139
|
+
Other render methods leave directives literal. See the
|
|
140
|
+
[include reference](docs/reference.md#file-includes) for budgets, AST expansion,
|
|
141
|
+
targets, and containment behavior.
|
|
142
|
+
|
|
143
|
+
## Security
|
|
144
|
+
|
|
145
|
+
Use `safe: true` and a restrictive profile for untrusted documents. Custom
|
|
146
|
+
renderer callbacks and symbol values are trusted output and may emit raw HTML.
|
|
147
|
+
The [untrusted-input reference](docs/reference.md#untrusted-input) describes
|
|
148
|
+
resource limits and URL handling.
|
|
149
|
+
|
|
150
|
+
## Development
|
|
151
|
+
|
|
152
|
+
Source builds, tests, and the carve-rs dependency pin are documented under
|
|
153
|
+
[Development](docs/reference.md#develop) and
|
|
154
|
+
[carve-rs dependency pin](docs/reference.md#carve-rs-dependency-pin).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Carve
|
|
4
|
+
# The released version of this gem. The gemspec derives `spec.version` from
|
|
5
|
+
# it, and an embedder quoting a version in a bug report is quoting this.
|
|
6
|
+
#
|
|
7
|
+
# It is not kept correct by hand on release: test/release_version_test.rb
|
|
8
|
+
# compares it against ext/carve/Cargo.toml and against the newest cut
|
|
9
|
+
# CHANGELOG section on every run, and .github/workflows/release.yml refuses to
|
|
10
|
+
# publish a gem whose version is not the tag being released.
|
|
11
|
+
VERSION = "0.1.10"
|
|
12
|
+
end
|
data/lib/carve.rb
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Carve: Ruby bindings for the Carve markup language.
|
|
4
|
+
#
|
|
5
|
+
# This file loads the compiled native extension (built from ext/carve, a Rust
|
|
6
|
+
# crate wrapping the carve-rs engine via magnus) and layers a small,
|
|
7
|
+
# idiomatic Ruby API on top of the raw primitives it exports.
|
|
8
|
+
require "json"
|
|
9
|
+
require_relative "carve/version"
|
|
10
|
+
|
|
11
|
+
# Load the compiled native extension. A precompiled platform gem ships one per
|
|
12
|
+
# Ruby ABI under lib/carve/<major.minor>/; a source install and `rake compile`
|
|
13
|
+
# build lib/carve/carve.so (or .bundle on macOS).
|
|
14
|
+
# Chosen by directory rather than by rescuing LoadError, so a binary that is
|
|
15
|
+
# present but cannot load (an older glibc, say) reports its own error.
|
|
16
|
+
abi = RUBY_VERSION[/\A\d+\.\d+/]
|
|
17
|
+
if File.directory?(File.join(__dir__, "carve", abi))
|
|
18
|
+
require_relative "carve/#{abi}/carve"
|
|
19
|
+
else
|
|
20
|
+
require_relative "carve/carve"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Carve
|
|
24
|
+
# Raised by .select_include_region when the region is missing or a marker is
|
|
25
|
+
# malformed, unpaired or duplicated. +rule+ is the engine's warning rule id.
|
|
26
|
+
class IncludeRegionError < ArgumentError
|
|
27
|
+
attr_reader :rule
|
|
28
|
+
|
|
29
|
+
def initialize(rule, name)
|
|
30
|
+
@rule = rule
|
|
31
|
+
super("#{rule}: region #{name.inspect} is missing or its markers are malformed")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Extension names the native binding understands.
|
|
36
|
+
#
|
|
37
|
+
# Taken from the engine itself rather than typed out here. This was a literal
|
|
38
|
+
# array, and the native binding had its own match arm listing the same names,
|
|
39
|
+
# so an extension carve-rs gained was unreachable from Ruby until somebody
|
|
40
|
+
# updated both by hand - and nothing failed in the meantime.
|
|
41
|
+
#
|
|
42
|
+
# The names are the engine's kebab-case keys, exposed as symbols
|
|
43
|
+
# (`:"math-block"`). The snake_case spellings this binding has always taken
|
|
44
|
+
# (`:math_block`) and its short aliases (`:mermaid`, `:dot`, `:toc`) still
|
|
45
|
+
# work as ARGUMENTS; this constant lists the canonical names.
|
|
46
|
+
EXTENSIONS = _extension_names.map(&:to_sym).freeze
|
|
47
|
+
|
|
48
|
+
# Render modes the native binding understands.
|
|
49
|
+
#
|
|
50
|
+
# +:interactive+ (default) emits live HTML with client-script hooks (e.g.
|
|
51
|
+
# `<pre class="mermaid">`, `<details>`). +:static+ emits self-contained HTML
|
|
52
|
+
# for print / PDF / archival: it forces disclosure (`<details open>`) and
|
|
53
|
+
# pre-renders client-script constructs through the +renderers:+ callables,
|
|
54
|
+
# degrading to (escaped) source when a renderer is absent or fails.
|
|
55
|
+
MODES = %i[interactive static].freeze
|
|
56
|
+
|
|
57
|
+
# Renderer keys accepted by the +renderers:+ Hash (see .to_html). Each maps a
|
|
58
|
+
# construct's source to a self-contained HTML string emitted on the static
|
|
59
|
+
# path. +mermaid+ / +chart+ / +graphviz+ are callables `(String) -> String`;
|
|
60
|
+
# +math+ is `(String, display_bool) -> String`.
|
|
61
|
+
RENDERER_KEYS = %i[mermaid chart graphviz math].freeze
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
# Render +source+ to HTML with its <tt>{{ path }}</tt> includes expanded,
|
|
65
|
+
# contained to +root:+.
|
|
66
|
+
#
|
|
67
|
+
# Carve.to_html_with_includes(File.read(page), root: "/srv/site",
|
|
68
|
+
# source_path: page, symbols: {smile: "\u{1F604}"})
|
|
69
|
+
#
|
|
70
|
+
# +root:+ and +source_path:+ are absolute, and the source file lives inside
|
|
71
|
+
# the root. A relative value raises ArgumentError rather than rooting
|
|
72
|
+
# containment at the process working directory, which is arbitrary with
|
|
73
|
+
# respect to the document.
|
|
74
|
+
#
|
|
75
|
+
# Returns +{value:, warnings:, dependencies:, suppressedWarnings:}+. Paths
|
|
76
|
+
# in +warnings+ and +dependencies+ are relative to the root, so a report can
|
|
77
|
+
# be shown to a reader as it stands. Each warning's +includedBy+ lists the
|
|
78
|
+
# directives that reached its file, top-level first, as +{file:, line:, column:}+.
|
|
79
|
+
#
|
|
80
|
+
# Every render option +to_html+ takes is accepted and reaches the included
|
|
81
|
+
# children too: an extension, a symbol map or a profile means the same thing
|
|
82
|
+
# in a child as in the parent.
|
|
83
|
+
def to_html_with_includes(source, root:, source_path:, extensions: nil, mode: nil,
|
|
84
|
+
renderers: nil, symbols: nil, safe: false, profile: nil,
|
|
85
|
+
sections: true, max_depth: nil, max_bytes: nil,
|
|
86
|
+
max_resolver_calls: nil, max_warnings: nil)
|
|
87
|
+
render_with_includes(
|
|
88
|
+
source, target: "html", root: root, source_path: source_path,
|
|
89
|
+
extensions: extensions, mode: mode, renderers: renderers, symbols: symbols,
|
|
90
|
+
safe: safe, profile: profile, sections: sections, max_depth: max_depth,
|
|
91
|
+
max_bytes: max_bytes, max_resolver_calls: max_resolver_calls,
|
|
92
|
+
max_warnings: max_warnings
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The expanded document as an AST, in the shape +.parse+ returns.
|
|
97
|
+
#
|
|
98
|
+
# Carve.parse_with_includes(File.read(page), root: root, source_path: page)
|
|
99
|
+
# # => {value: {type: "document", ...}, warnings: [], dependencies: [...], ...}
|
|
100
|
+
#
|
|
101
|
+
# For a host that walks the tree rather than rendering HTML - carve-hexapdf
|
|
102
|
+
# draws its PDF from this shape.
|
|
103
|
+
#
|
|
104
|
+
# Nodes carry NO +:pos+, unlike +.parse+. Spec I4 leaves position remapping
|
|
105
|
+
# out of scope in every engine, so a span on an included node would name an
|
|
106
|
+
# offset in a document the caller never passed.
|
|
107
|
+
def parse_with_includes(source, root:, source_path:, extensions: nil, profile: nil,
|
|
108
|
+
max_depth: nil, max_bytes: nil, max_resolver_calls: nil,
|
|
109
|
+
max_warnings: nil)
|
|
110
|
+
render_with_includes(
|
|
111
|
+
source, target: "ast", root: root, source_path: source_path,
|
|
112
|
+
extensions: extensions, profile: profile, max_depth: max_depth,
|
|
113
|
+
max_bytes: max_bytes, max_resolver_calls: max_resolver_calls,
|
|
114
|
+
max_warnings: max_warnings
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# The include pass over any render target: +"html"+, +"markdown"+,
|
|
119
|
+
# +"plain"+, +"ansi"+ or +"ast"+.
|
|
120
|
+
#
|
|
121
|
+
# There is no +"carve"+ target. Spec I15 excludes the Carve writer from
|
|
122
|
+
# expansion, because inlining a child into the formatter's output rewrites
|
|
123
|
+
# the author's document rather than formatting it.
|
|
124
|
+
def render_with_includes(source, root:, source_path:, target: "html", extensions: nil,
|
|
125
|
+
mode: nil, renderers: nil, symbols: nil, safe: false,
|
|
126
|
+
profile: nil, sections: true, max_depth: nil, max_bytes: nil,
|
|
127
|
+
max_resolver_calls: nil, max_warnings: nil)
|
|
128
|
+
JSON.parse(
|
|
129
|
+
_render_with_includes_json(
|
|
130
|
+
source.to_s,
|
|
131
|
+
root.to_s,
|
|
132
|
+
source_path.to_s,
|
|
133
|
+
target.to_s,
|
|
134
|
+
Array(extensions).map(&:to_s),
|
|
135
|
+
(mode || :interactive).to_s,
|
|
136
|
+
renderers || {},
|
|
137
|
+
symbols || {},
|
|
138
|
+
!!safe,
|
|
139
|
+
profile&.to_s,
|
|
140
|
+
!!sections,
|
|
141
|
+
max_depth,
|
|
142
|
+
max_bytes,
|
|
143
|
+
max_resolver_calls,
|
|
144
|
+
max_warnings,
|
|
145
|
+
),
|
|
146
|
+
symbolize_names: true,
|
|
147
|
+
# The engine bounds nesting itself, above Ruby JSON's default of 100, so
|
|
148
|
+
# a legal document would otherwise raise here. Same reason as .parse.
|
|
149
|
+
max_nesting: false,
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Format +source+ into canonical Carve. A writer refusal returns the source
|
|
154
|
+
# as authored. +align_tables:+ pads table cells into aligned columns.
|
|
155
|
+
def to_carve(source, align_tables: false)
|
|
156
|
+
_to_carve(source, align_tables ? true : false)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Resolve <tt>@region:name</tt> in a file's text with the engine's marker
|
|
160
|
+
# rules, so an editor jumping to a region agrees with the render.
|
|
161
|
+
#
|
|
162
|
+
# Carve.select_include_region(text, "setup", mode: :carve) # => 3..4
|
|
163
|
+
#
|
|
164
|
+
# Returns the 1-based, inclusive Range of content lines; an empty region is
|
|
165
|
+
# an empty Range such as +2..1+. +mode: :carve+ accepts only
|
|
166
|
+
# <tt>%% #region</tt> markers, +mode: :code+ also the source-comment forms
|
|
167
|
+
# of an +@code+ include. Raises IncludeRegionError otherwise.
|
|
168
|
+
def select_include_region(source, name, mode:)
|
|
169
|
+
result = _select_include_region(source, name.to_s, mode.to_s)
|
|
170
|
+
raise IncludeRegionError.new(result, name.to_s) if result.is_a?(String)
|
|
171
|
+
|
|
172
|
+
result[0]..result[1]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Import HTML or Markdown into canonical Carve. Both methods return the
|
|
176
|
+
# shared migration shape `{ value:, report: }`.
|
|
177
|
+
def from_html(source, mode: :safe)
|
|
178
|
+
JSON.parse(_from_html_json(source, mode.to_s), symbolize_names: true)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def from_markdown(source)
|
|
182
|
+
result = JSON.parse(_from_markdown_json(source), symbolize_names: true)
|
|
183
|
+
# Keep the 0.1.x spelling while exposing the shared cross-language key.
|
|
184
|
+
result[:report][:source_format] = result[:report][:sourceFormat]
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Render Carve +source+ to an HTML string.
|
|
189
|
+
#
|
|
190
|
+
# With no extensions:
|
|
191
|
+
# Carve.to_html("# Hello") # => "<section ...>\n <h1>Hello</h1>..."
|
|
192
|
+
#
|
|
193
|
+
# With extensions (Array of names as Symbols or Strings):
|
|
194
|
+
# Carve.to_html(src, extensions: [:math_block])
|
|
195
|
+
# Carve.to_html(src, extensions: %w[math-block list-table])
|
|
196
|
+
#
|
|
197
|
+
# Recognized extension names: see Carve::EXTENSIONS. Names may be given
|
|
198
|
+
# snake_case (:math_block) or hyphenated ("math-block").
|
|
199
|
+
#
|
|
200
|
+
# An unknown extension name raises ArgumentError (from the native layer).
|
|
201
|
+
#
|
|
202
|
+
# ==== Static render mode
|
|
203
|
+
#
|
|
204
|
+
# Pass +mode: :static+ (or "static") to emit self-contained HTML for print /
|
|
205
|
+
# PDF / archival. In static mode disclosure is forced (`<details open>`) and
|
|
206
|
+
# client-script constructs are pre-rendered through the +renderers:+ Hash:
|
|
207
|
+
#
|
|
208
|
+
# Carve.to_html(src, extensions: [:fenced_render], mode: :static,
|
|
209
|
+
# renderers: { mermaid: ->(s) { "<svg>#{s}</svg>" } })
|
|
210
|
+
#
|
|
211
|
+
# Renderer callables (Symbol or String keys, see Carve::RENDERER_KEYS):
|
|
212
|
+
# * +:mermaid+ / +:chart+ / +:graphviz+ -> callable `(String) -> String`
|
|
213
|
+
# * +:math+ -> callable `(String, display) -> String`
|
|
214
|
+
#
|
|
215
|
+
# When a needed renderer is absent, or a renderer raises / returns a
|
|
216
|
+
# non-String, the construct degrades to its HTML-ESCAPED source (never blank,
|
|
217
|
+
# never raw HTML). Omitting +mode:+ defaults to interactive (non-breaking).
|
|
218
|
+
#
|
|
219
|
+
# An unknown mode or renderer key raises ArgumentError (from the native
|
|
220
|
+
# layer).
|
|
221
|
+
#
|
|
222
|
+
# ==== Symbols
|
|
223
|
+
#
|
|
224
|
+
# A +:name:+ symbol renders its literal +:name:+ source unless the name is
|
|
225
|
+
# in the +symbols:+ Hash (String or Symbol keys, String values):
|
|
226
|
+
#
|
|
227
|
+
# Carve.to_html("Ship it :rocket: :shrug:", symbols: { "rocket" => "🚀" })
|
|
228
|
+
# # => "<p>Ship it 🚀 :shrug:</p>" (an unmapped name stays literal)
|
|
229
|
+
#
|
|
230
|
+
# The leading word-boundary guard is unaffected by an active map: +a:b:c+,
|
|
231
|
+
# +10:30:+ and +me@example.com+ never become symbols. A non-String value
|
|
232
|
+
# raises TypeError (from the native layer).
|
|
233
|
+
#
|
|
234
|
+
# SECURITY: a mapped symbol value is inserted as TRUSTED RAW output in the
|
|
235
|
+
# target format - it is NOT escaped, the same trust class as a +renderers:+
|
|
236
|
+
# callable. <tt>{ "b" => "<b>x</b>" }</tt> emits a real +<b>+ element, not
|
|
237
|
+
# escaped text. This is deliberate: processor configuration is trusted.
|
|
238
|
+
# NEVER build a symbols map out of untrusted / user-supplied input.
|
|
239
|
+
#
|
|
240
|
+
# ==== Section wrappers
|
|
241
|
+
#
|
|
242
|
+
# A top-level heading is wrapped, along with the content following it up to
|
|
243
|
+
# the next same-or-shallower heading, in a +<section>+ carrying the heading's
|
|
244
|
+
# id (spec PART 9 §13). Only the id moves - +{#install .featured}+ gives
|
|
245
|
+
# <tt><section id="install"><h2 class="featured"></tt> - and a heading inside
|
|
246
|
+
# a blockquote, div or list item is not wrapped at all.
|
|
247
|
+
#
|
|
248
|
+
# Pass +sections: false+ to render headings flat, with the id back on the
|
|
249
|
+
# +<h*>+:
|
|
250
|
+
#
|
|
251
|
+
# Carve.to_html("# A\n\np\n")
|
|
252
|
+
# # => "<section id=\"A\">\n <h1>A</h1>\n <p>p</p>\n</section>"
|
|
253
|
+
# Carve.to_html("# A\n\np\n", sections: false)
|
|
254
|
+
# # => "<h1 id=\"A\">A</h1>\n<p>p</p>"
|
|
255
|
+
#
|
|
256
|
+
# This is for a host whose CSS or JS assumes rendered blocks are direct
|
|
257
|
+
# children of the content container - the <tt>.stack > * + *</tt> spacing
|
|
258
|
+
# idiom, +:first-child+, +nth-child()+ counting, DOM child walks - all of
|
|
259
|
+
# which stop matching once a wrapper sits in between. It is the one output
|
|
260
|
+
# change that breaks a document whose *source* migrated cleanly.
|
|
261
|
+
#
|
|
262
|
+
# Nothing else changes: ids, collision dedup, <tt></#id></tt>
|
|
263
|
+
# cross-references, implicit +[Heading][]+ references and heading numbering
|
|
264
|
+
# all resolve against the slug rather than the element carrying it. The
|
|
265
|
+
# endnotes <tt><section role="doc-endnotes"></tt> is a separate construct and
|
|
266
|
+
# is still emitted.
|
|
267
|
+
#
|
|
268
|
+
# ==== Untrusted input
|
|
269
|
+
#
|
|
270
|
+
# Carve's normative hardening is always on and needs no argument here:
|
|
271
|
+
# dangerous URL schemes are blanked, event-handler attributes such as
|
|
272
|
+
# +onclick+ are dropped, and the bidi override / isolate characters behind
|
|
273
|
+
# Trojan Source are removed from rendered text.
|
|
274
|
+
#
|
|
275
|
+
# Raw passthrough is the deliberate exception - a <tt>```=html</tt> block or
|
|
276
|
+
# a <tt>`...`{=html}</tt> span renders VERBATIM by design - so it is the one
|
|
277
|
+
# thing input you did not author has to switch off:
|
|
278
|
+
#
|
|
279
|
+
# Carve.to_html(user_input, safe: true, profile: :comment)
|
|
280
|
+
#
|
|
281
|
+
# +safe:+ escapes those raw blocks and spans instead of emitting them.
|
|
282
|
+
# +profile:+ restricts which constructs are allowed at all and caps input
|
|
283
|
+
# length: +:full+, +:article+, +:comment+ or +:minimal+ (String or Symbol),
|
|
284
|
+
# +nil+ for no profile. An unknown profile name raises ArgumentError (from
|
|
285
|
+
# the native layer) rather than being ignored.
|
|
286
|
+
#
|
|
287
|
+
# A profile REJECTION also raises ArgumentError - input past the profile's
|
|
288
|
+
# +max_length+, or a denied construct when the profile's action is error:
|
|
289
|
+
#
|
|
290
|
+
# Carve.to_html("x" * 20_000, profile: :minimal)
|
|
291
|
+
# # ArgumentError: Profile violations: 'document' is not allowed:
|
|
292
|
+
# # max_length_exceeded (...)
|
|
293
|
+
#
|
|
294
|
+
# It is an exception rather than a return value because the engine's
|
|
295
|
+
# infallible entry point answers a rejection with an empty String, which a
|
|
296
|
+
# caller cannot tell from a document that legitimately rendered to nothing.
|
|
297
|
+
def to_html(source, extensions: nil, mode: nil, renderers: nil, symbols: nil,
|
|
298
|
+
safe: false, profile: nil, sections: true)
|
|
299
|
+
list = Array(extensions)
|
|
300
|
+
|
|
301
|
+
# Fast path: interactive (default), no extensions, no renderers, no
|
|
302
|
+
# symbols, neither safe-render control in play, and section wrapping left
|
|
303
|
+
# on. `sections: false` MUST fall through -- `_to_html` takes no options,
|
|
304
|
+
# so short-circuiting to it would drop the flag and return wrapped output
|
|
305
|
+
# with no error, which is the failure mode a caller cannot see.
|
|
306
|
+
if list.empty? && (mode.nil? || mode.to_s == "interactive") &&
|
|
307
|
+
(renderers.nil? || renderers.empty?) && (symbols.nil? || symbols.empty?) &&
|
|
308
|
+
!safe && profile.nil? && sections
|
|
309
|
+
return _to_html(source.to_s)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
_to_html_safe(
|
|
313
|
+
source.to_s,
|
|
314
|
+
list.map(&:to_s),
|
|
315
|
+
(mode || :interactive).to_s,
|
|
316
|
+
renderers || {},
|
|
317
|
+
symbols || {},
|
|
318
|
+
!!safe,
|
|
319
|
+
profile&.to_s,
|
|
320
|
+
!!sections,
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# Read a document's provenance marker, as written by +carve fmt --stamp+.
|
|
326
|
+
#
|
|
327
|
+
# Carve.read_stamp(source)
|
|
328
|
+
# # => {version: "0.1", generated_by: "carve-php 0.1.0"}
|
|
329
|
+
# Carve.read_stamp("# Hi\n")
|
|
330
|
+
# # => nil
|
|
331
|
+
#
|
|
332
|
+
# Returns +nil+ when the document carries no marker. That is the normal case
|
|
333
|
+
# for a hand-written document and means "unknown", not "current".
|
|
334
|
+
#
|
|
335
|
+
# Both documented marker forms are read - the trailing +%%+ line and the
|
|
336
|
+
# +%%%+ block - and a marker written by any Carve engine reads the same,
|
|
337
|
+
# which is the point of recording it.
|
|
338
|
+
def read_stamp(source)
|
|
339
|
+
_read_stamp(source.to_s)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Whether a document was last processed under an older spec version than
|
|
343
|
+
# this engine targets, so the +[behavior]+ changelog entries between the two
|
|
344
|
+
# are worth reviewing.
|
|
345
|
+
#
|
|
346
|
+
# Carve.needs_review?(stored_document)
|
|
347
|
+
#
|
|
348
|
+
# An UNSTAMPED document answers true: its provenance is unknown, and
|
|
349
|
+
# assuming it is current is the unsafe direction. Pass +current_version+ to
|
|
350
|
+
# compare against something other than this engine's spec version.
|
|
351
|
+
#
|
|
352
|
+
# See https://markup-carve.github.io/carve/versioning for what a version
|
|
353
|
+
# difference means for a stored document.
|
|
354
|
+
def needs_review?(source, current_version = nil)
|
|
355
|
+
_stamp_needs_review(source.to_s, current_version&.to_s)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Parse Carve +source+ into an AST: a tree of Ruby Hashes and Arrays.
|
|
359
|
+
#
|
|
360
|
+
# Carve.parse("# Hi")
|
|
361
|
+
# # => {type: "document",
|
|
362
|
+
# # children: [{type: "heading", level: 1,
|
|
363
|
+
# # children: [{type: "text", value: "Hi"}], attrs: nil}],
|
|
364
|
+
# # srcByteLength: 4, astVersion: "1.2"}
|
|
365
|
+
#
|
|
366
|
+
# The root carries exactly +:type+, +:children+, +:srcByteLength+ and
|
|
367
|
+
# +:astVersion+ (spec
|
|
368
|
+
# PART 12 §7). Frontmatter and footnote definitions are block nodes in
|
|
369
|
+
# +:children+, not root fields: a root field cannot carry the position §4
|
|
370
|
+
# requires of every node, and both are source an editor navigates to.
|
|
371
|
+
# Frontmatter is the first child, carrying +:format+ and RAW +:content+.
|
|
372
|
+
#
|
|
373
|
+
# Every node is a Hash with a +:type+ key plus its fields; child collections
|
|
374
|
+
# are Arrays; +:attrs+ is +nil+ or a Hash of +{id:, classes:, key_values:}+.
|
|
375
|
+
# Keys are symbols. This is the raw parse tree (default profile, no
|
|
376
|
+
# extensions), suitable for a custom renderer (e.g. Carve -> PDF).
|
|
377
|
+
def parse(source)
|
|
378
|
+
# max_nesting: false - the engine already bounds nesting (its own
|
|
379
|
+
# MAX_NESTING_DEPTH cap), and that cap exceeds Ruby JSON's default
|
|
380
|
+
# max_nesting of 100. Without this, deeply-nested-but-valid documents
|
|
381
|
+
# that Carve.to_html renders fine would raise JSON::NestingError here.
|
|
382
|
+
JSON.parse(_to_ast_json(source.to_s), symbolize_names: true, max_nesting: false)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: carve-lang
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.10
|
|
5
|
+
platform: aarch64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- markup-carve
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-23 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake-compiler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.2'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.2'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.0'
|
|
55
|
+
description: |-
|
|
56
|
+
Parse and render Carve markup to HTML from Ruby. A native extension
|
|
57
|
+
(magnus + rb-sys) over the carve-rs engine, mirroring how Djot's djotter
|
|
58
|
+
gem wraps the jotdown crate. No parser is reimplemented in Ruby.
|
|
59
|
+
email:
|
|
60
|
+
executables: []
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- CHANGELOG.md
|
|
65
|
+
- LICENSE
|
|
66
|
+
- README.md
|
|
67
|
+
- lib/carve.rb
|
|
68
|
+
- lib/carve/3.0/carve.so
|
|
69
|
+
- lib/carve/3.1/carve.so
|
|
70
|
+
- lib/carve/3.2/carve.so
|
|
71
|
+
- lib/carve/3.3/carve.so
|
|
72
|
+
- lib/carve/3.4/carve.so
|
|
73
|
+
- lib/carve/4.0/carve.so
|
|
74
|
+
- lib/carve/version.rb
|
|
75
|
+
homepage: https://github.com/markup-carve/carve-rb
|
|
76
|
+
licenses:
|
|
77
|
+
- MIT
|
|
78
|
+
metadata:
|
|
79
|
+
rubygems_mfa_required: 'true'
|
|
80
|
+
post_install_message:
|
|
81
|
+
rdoc_options: []
|
|
82
|
+
require_paths:
|
|
83
|
+
- lib
|
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.0'
|
|
89
|
+
- - "<"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 4.1.dev
|
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
requirements: []
|
|
98
|
+
rubygems_version: 3.5.23
|
|
99
|
+
signing_key:
|
|
100
|
+
specification_version: 4
|
|
101
|
+
summary: Native Ruby bindings for the Carve markup language.
|
|
102
|
+
test_files: []
|