carve-lang 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d63ffac925c5ff16de3889adf7e5e887cd816a4900cf46304f5da865e00998c2
4
- data.tar.gz: cb52a4f8c5c3edca31be9e70c7093ff375aa07ae06d48f240bb15b76d8539691
3
+ metadata.gz: 9dc82ae49fa620a500b09b60ad705caca3488b99c18ec3dda8ff22f1aabc5b55
4
+ data.tar.gz: 192a18307bfb8cc9bd9364a333592b92bf0cd9075f2403fa602b09307705912b
5
5
  SHA512:
6
- metadata.gz: b7c2b1b33a8a979ac4ba2f4b8986e7876188ebe8105c5cb9beb9d55e7f068a04f9bcd785b5837309c5fd408670994c57e3706e39979b36adf1a0c8cbb8226738
7
- data.tar.gz: a676302d1f1b343abf78e324bdd48dcedd5146e6637370ef3276f33cab5479c854d149f3b9365865867d1f008b1fe3af086b2201ac1b41e79dbb12c31a3f402c
6
+ metadata.gz: a0eb63cc0fb830f1d6a69136d7a934931069718a700aa280017313e05a704614ef805bd9a136ce17620fd88188bb39cd4e335e059ecb002683a7a7bf4a646c1c
7
+ data.tar.gz: 33039444b491c79db93ef2041556d4ec40533ec98ed1dec8cc154c79b4b147ffde9a8533bf8df0839ac72c74da352d5087251c1eccb5521d769d0ff3a202a035
data/CHANGELOG.md CHANGED
@@ -7,6 +7,294 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.2] - 2026-08-27
11
+
12
+ ### Security
13
+
14
+ - The gem is published through RubyGems trusted publishing rather than a
15
+ long-lived API key, and the gemspec sets `rubygems_mfa_required`, so RubyGems
16
+ refuses a push from an account without MFA (#81).
17
+
18
+ ### Changed
19
+
20
+ - The engine moves from carve-rs `0.1.3` (`a33c42ad`), which `0.1.1` shipped, to
21
+ `bd19414d` - 106 commits across four pin bumps (#80, #82, #84, #87). Rendering
22
+ changes an existing document can see: a column's alignment defaults come from
23
+ the header section, an explicit alignment run is validated and inherited, and
24
+ a table cell's marker run must end at a space; tables gained semantic row
25
+ partitions and a list table can carry local headers; a braced hyphen pair is
26
+ an en dash, an empty brace pair is text, and the doubled run is the canonical
27
+ arrow; a hyphen run opening a word after whitespace is a flag, not a dash; a
28
+ continuation marker attaches only a flush-left block; the footnote backlink
29
+ has an accessible name; a caption no longer disables a diagram fence's preset;
30
+ the first code group is numbered like every other one; a heading at a content
31
+ column leaves no paragraph open, and a definition at a list item's content
32
+ column closes its paragraph; an all-blank raw payload stays distinct from an
33
+ absent one; and depth limits guard every recursive parse, render and AST-JSON
34
+ path. `<thead>` and `<tfoot>` also write one row per line, which changes the
35
+ emitted HTML without changing what it renders. The last bump adds: three blank
36
+ lines are a hard list boundary rather than a wide gap between two items, so a
37
+ document that used the run as spacing now parses as two lists; a heading id
38
+ can opt in to ASCII folding; a tab control renders as a button and a code
39
+ group takes the mode option; and a `%%` line a verbatim run has already
40
+ swallowed no longer produces a comment node after that run, which changes the
41
+ tree without changing the HTML (#84).
42
+ The fourth bump is 18 further commits, and over the spec corpus exactly one
43
+ document's HTML moves with it: a list marker at an item's content column
44
+ opens a sublist whether or not it is the item's first, so a marker that
45
+ follows a blank line and a paragraph inside an item no longer joins that
46
+ paragraph (markup-carve/carve-rs#1251). A further 104 documents change only
47
+ their reported positions, because a container's span now starts at its
48
+ opening markup and ends at its last placed child, and a definition list's
49
+ likewise (markup-carve/carve-rs#1238, markup-carve/carve-rs#1248,
50
+ markup-carve/carve-rs#1252). Nothing else an existing document can see
51
+ changes (#87).
52
+ - The engine then moves from `bd19414d` to `f2fbb24c` - 39 further commits
53
+ (#91). A container with no closer ends at its last placed child rather than
54
+ one codepoint past it, which is the PART 12 §4 extent defect this gem was
55
+ still the one implementation reporting (markup-carve/carve-rs#1308, #89); a
56
+ footnote continuation survives a blank run (markup-carve/carve-rs#1295); and
57
+ a list takes PART 9 §17 L7's consumed `loose` attribute, which loosens the
58
+ list instead of being emitted on it and which `Carve.parse` publishes on a
59
+ definition list as a `loose` field (markup-carve/carve-rs#1304). Seven corpus
60
+ documents move with the bump and all seven are documents the corpus gained,
61
+ so nothing that already rendered correctly changed.
62
+ - The embedded engine moves to carve-rs `42a952cb`. An indented lone image now
63
+ parses as a paragraph holding an inline image rather than a block image
64
+ (markup-carve/carve#1660, markup-carve/carve-rs#1347). AST only - the
65
+ rendered HTML is unchanged on every corpus document (#93).
66
+ - The embedded engine moves from `42a952cb` to `468e71af` - 70 further commits
67
+ across three pin bumps (#96, #97, #98), and each of the three changes what an
68
+ existing document renders as. The first two settle one rule between them: a
69
+ list item's content column is measured from the bare marker, so neither the
70
+ task checkbox nor a marker-attached attribute block widens it
71
+ (markup-carve/carve-rs#1364, markup-carve/carve-rs#1374,
72
+ markup-carve/carve-rs#1379). `-{#k} [x] a` has content column 2, so a `# h`
73
+ written under it at column 2 opens a heading inside the item where the wider
74
+ column made it lazy paragraph text. Also in those two: a lone image at an
75
+ item's content column keeps the item's looseness, restoring the `<p>` wrapper
76
+ the item used to drop (markup-carve/carve-rs#1359); the ANSI quote bar
77
+ reports containment, so a quoted heading, code block, table, thematic break
78
+ and lone image keep the bar and a quoted list's bar sits outside its marker
79
+ (markup-carve/carve-rs#1363); and on the import side an imported heading
80
+ records its id, an imported task item comes back a task item, an input's type
81
+ matches the checkbox keyword case-insensitively, an HTML comment imports as a
82
+ Carve comment, and an imported code block no longer gains a trailing blank
83
+ line on every pass (markup-carve/carve-rs#1361, markup-carve/carve-rs#1366,
84
+ markup-carve/carve-rs#1378, markup-carve/carve-rs#1386,
85
+ markup-carve/carve-rs#1384).
86
+ The third bump is 44 commits on its own, and rendered HTML moves on 50 corpus
87
+ documents with it. Measured against the corpus's own expected output at the
88
+ spec the new pin pins, the old pin differed on all 50 and the new pin on
89
+ none, so those 50 were being rendered wrongly and this is a correctness fix
90
+ rather than a preference. What an author notices is that a recognized block
91
+ opener written past a container's content column now opens that block inside
92
+ the container instead of folding into text: an indented `> q` under a list
93
+ item wrote `<p>&gt; q</p>` and now writes a block quote, an indented `# h`
94
+ wrote `<p># h</p>` and now writes a heading, an indented code fence folded
95
+ into inline code inside a paragraph and now writes a `<pre>` block, and a
96
+ tab-indented `> quote` wrote text on the item's own line and now writes a
97
+ block quote; where a blank line preceded the block, the item is no longer
98
+ reported loose either. The same authored base applies inside a footnote body
99
+ and a definition description, so a table or a link reference definition
100
+ written past the body's column registers there (markup-carve/carve-rs#1387,
101
+ markup-carve/carve-rs#1420, markup-carve/carve-rs#1422,
102
+ markup-carve/carve-rs#1425, markup-carve/carve-rs#1431,
103
+ markup-carve/carve-rs#1432, markup-carve/carve-rs#1433, measured over the
104
+ corpus that markup-carve/carve-rs#1417 pins). Four further changes an
105
+ existing document can see ride along: an explicit id or class may begin with
106
+ an ASCII digit, so `::: 123` opens a div with that class instead of staying a
107
+ paragraph (markup-carve/carve-rs#1393); `::: >` spells a block quote with no
108
+ marker column, and its closing fence takes a caption
109
+ (markup-carve/carve-rs#1399, markup-carve/carve-rs#1411); reference and
110
+ footnote labels differing only in ASCII whitespace resolve to one definition
111
+ (markup-carve/carve-rs#1397); and `{align=left|right|center}` on an element
112
+ whose `align` means text alignment renders a CSS declaration rather than the
113
+ deprecated presentational attribute (markup-carve/carve-rs#1412).
114
+ `Carve.to_carve` also writes one space after a definition separator
115
+ (markup-carve/carve-rs#1426) (#98).
116
+ - The final release pin moves from `0c54d2ea` to the released carve-rs 0.1.4
117
+ (`2e9c43f2`). This closes every declared spec-drift window: link and
118
+ abbreviation definitions below a definition body's column follow the same
119
+ folding rule, a comment in a footnote body leaves no blank line behind, the
120
+ continuation marker's column gate reaches every container, an empty
121
+ description body writes the `{empty}` sentinel, citation items are typed and
122
+ positioned, and paragraph block-image promotion is published on the AST.
123
+ - The stale pin is what made this gem the only implementation failing PART 12
124
+ conformance in markup-carve/carve#1451 - a binding has no vote of its own, and
125
+ this one was voting with an engine 28 commits behind (#82). It recurred within
126
+ two days, on three documents and a pin one day old, because nothing in this
127
+ repository compares the tree this gem produces against the engine's own (#84).
128
+ It recurred again the next day, on one document, with that comparison still
129
+ unmerged (#86) and the pin 18 commits behind (#87). It recurred twice more
130
+ inside this window: at 39 commits behind, still carrying the PART 12 §4
131
+ extent defect (#89, #91), and again on a ruling only the tree can see,
132
+ which a byte-for-byte HTML comparison cannot detect (#93).
133
+
134
+ ### Added
135
+
136
+ - `Carve.from_html(source, mode: :safe)` and `Carve.from_markdown(source)`
137
+ import an existing document into canonical Carve (#92). Both return the
138
+ shared migration shape `{value:, report:}`: `value` is the Carve source, and
139
+ `report` carries `diagnostics`, each with `code`, `message`, `severity` and,
140
+ where the engine placed one, `path`. `from_html` takes `:safe`, `:semantic`
141
+ or `:roundtrip` and reports back the `mode` and `adapter` it ran with; any
142
+ other mode raises `ArgumentError`. `from_markdown` reports
143
+ `source_format: "markdown"` and diagnoses nothing.
144
+ - `Carve.to_markdown`, `to_plain_text`, `to_ansi` and `to_carve`, so every core
145
+ render target the embedded engine already understood is reachable from Ruby
146
+ (#88). `Carve.to_html` and the AST entry points are unchanged.
147
+
148
+ ## [0.1.1] - 2026-08-18
149
+
150
+ Everything below is the delta from `v0.1.0`, which is the only version a
151
+ reader can be upgrading from. The engine pin moved seven times inside this
152
+ window and lands on carve-rs `0.1.3` (`a33c42ad`); the intermediate revisions
153
+ are not listed, because no release ever shipped them.
154
+
155
+ ### Security
156
+
157
+ - **A list-valued URL attribute is probed at every candidate, not at its head**
158
+ (PART 9 §25, markup-carve/carve#1320). The sanitizer read only the value's
159
+ leading scheme, which vouches for the whole value only where the whole value
160
+ is one URL, so `srcset="safe.png 1x, javascript:alert(1) 2x"` passed on its
161
+ second entry. `srcset`, `imagesrcset`, `ping` and `attributionsrc` are now
162
+ split into tokens and every candidate is read, and any hit blanks the whole
163
+ value. The engine embedded in `0.1.0` predates the fix, so the gem published
164
+ on 2026-07-12 carries the defect.
165
+
166
+ ### Added
167
+
168
+ - **`Carve.parse` publishes source positions.** Every block node the engine can
169
+ place carries `pos` with `startLine`, `endLine`, `startColumn`, `endColumn`,
170
+ `startOffset` and `endOffset`, as PART 12 §4 spells it: lines and columns
171
+ 1-based, offsets 0-based, counted in Unicode codepoints, `endColumn` and
172
+ `endOffset` exclusive. A node whose span the engine could not determine has
173
+ no `pos` key at all rather than one holding placeholder numbers - an absent
174
+ position is a fact a consumer can act on, an invented one is not. Inline
175
+ nodes do not carry positions yet (markup-carve/carve-rs#333).
176
+
177
+ - **`Carve.to_html(source, sections: false)` renders headings without the
178
+ `<section>` wrapper** (#36, PART 9 §13). The id goes back on the `<h*>`
179
+ alongside its other attributes and the blocks that would have been section
180
+ children stay siblings. Default `true`, so existing output is unchanged. The
181
+ wrapper is the one output change that breaks a site whose source migrated
182
+ cleanly: CSS and JS assuming rendered blocks are direct children of the
183
+ content container stop matching once a `<section>` sits in between.
184
+
185
+ - **Every extension the engine registers is reachable from Ruby.**
186
+ `Carve::EXTENSIONS` lists 31 names instead of 15, adding glossary, index,
187
+ table-of-contents placement (`::: toc`), heading numbers, heading references,
188
+ heading level shift, code groups, tabs, the img fence, color swatches, smart
189
+ quotes, and the remaining fenced-render presets (PlantUML, D2, WaveDrom,
190
+ Vega-Lite, ABC). The list comes from the engine's registry rather than being
191
+ typed out in the native binding and again in `lib/carve.rb`. Canonical names
192
+ are the engine's kebab-case keys; snake_case spellings and the short aliases
193
+ (`:math`, `:permalinks`, `:mermaid`, `:dot`, `:chart`, `:toc`) still work.
194
+
195
+ - **Composite figures** (PART 9 §4c). A **bare** `::: figure` fence is no longer
196
+ a generic container: it is one figure of ordered panels, rendering
197
+ `<figure class="carve-figure-group">` around a
198
+ `<div class="carve-figure-panels">` whose members are each a
199
+ `<figure class="carve-figure-panel">`, and a `^ ` line following the closer
200
+ becomes the group's `<figcaption>` instead of an ordinary paragraph. A fence
201
+ carrying a title or a `[label]` keeps the old shape, so a document that named
202
+ its figure divs renders as before. `Carve.parse` publishes the group as a new
203
+ `figure_group` node type.
204
+
205
+ ### Changed
206
+
207
+ - **Breaking (`Carve.parse`): the published tree now matches the reference
208
+ shape.** A tree from this binding did not interoperate with carve-js or
209
+ carve-php, so several names and the root's shape moved at once:
210
+
211
+ | was | now |
212
+ |---|---|
213
+ | `critic_insert` / `critic_delete` / `critic_substitute` | `insert` / `delete` / `substitution` |
214
+ | `footnote` (both forms) | `footnote_ref` / `inline_footnote` |
215
+ | `ref_id` | `refId` |
216
+ | `footnote_defs` | `footnoteDefs` |
217
+ | `source_len` | `srcByteLength` |
218
+ | `emoji` | `symbol` (and it carries `attrs`) |
219
+
220
+ The root now carries exactly `type`, `children` and `srcByteLength`.
221
+ Frontmatter and footnote definitions are block nodes in `children` rather
222
+ than root fields, which PART 12 §7 requires: a root field cannot carry the
223
+ position §4 requires of every node, and both are source an editor navigates
224
+ to (carve#411, carve#418). Frontmatter is the first child, carrying `format`
225
+ and **raw** `content` - not parsed key/values, which could not represent a
226
+ `---toml` block at all. Anything reading `ast[:frontmatter]` or
227
+ `ast[:footnoteDefs]` breaks.
228
+
229
+ - **Breaking (`Carve.parse`): three constructs became their own node types.** A
230
+ backslash escape is `{"type":"escaped_text","value":"-"}` instead of being
231
+ folded into surrounding text - the backslash carries intent the character does
232
+ not, since an author writes `\-\-` precisely so a consumer will not render an
233
+ en dash (carve#350). A `::: |` fence is `line_block` instead of a `div` with a
234
+ `.line-block` class, because inside it every newline is a hard break and a
235
+ class alone could not say which one a node was (carve#359). A block
236
+ extension's `summary` is a list of inline nodes rather than a plain string,
237
+ matching the admonition `title` shape. Definition lists publish
238
+ `definition_term` and `definition_description` nodes
239
+ (markup-carve/carve-rs#374).
240
+
241
+ These are additive for a consumer with a default branch and a compile error
242
+ for one without.
243
+
244
+ - **Delimited inline comments carried in from the engine.** `{% … %}` is an
245
+ inline comment that ends at its closer, alongside the trailing `%%` form that
246
+ runs to the end of its inline run (PART 9 §21a). Both render nothing; in
247
+ `Carve.parse` they are the same `comment` node, told apart by a `delimited`
248
+ field, because the two spell different documents and a writer has to
249
+ reproduce the one that was written.
250
+
251
+ - **Language changes carried in from the engine.** Superscript and subscript are
252
+ braced-only - bare `^x^` and `,x,` are literal text, the markup is `{^x^}` and
253
+ `{,x,}`. The `:name:` symbol inline was added, with a leading word-boundary
254
+ guard so `a:b:c`, `10:30:` and `me@example.com` stay literal. Compact semantic
255
+ spans arrived: `[Tab]{kbd}` renders `<kbd>Tab</kbd>`, with `abbr`, `time` and
256
+ `kbd` in core and `samp`, `var`, `cite` and `dfn` behind the `semantic-span`
257
+ extension.
258
+
259
+ Every `<th>` carries a `scope` - `col` in the header run, `row` for a
260
+ `|=` cell in a body row. A cell's attribute block binds after its kind and
261
+ alignment markers, so a `{...}` following a cell's `=` marker is read as
262
+ attributes rather than rendered as cell text. A mandatory base class merges
263
+ into the author's class slot at its authored position rather than leading it,
264
+ so `:widget[x]{#i .c}` keeps `id` first.
265
+
266
+ - **Rendering corrections carried in from the engine.** A block-attribute line
267
+ reaches the nested list it was written for, and a flush-left attribute line is
268
+ read as attributes rather than as paragraph text. A figure group holds its
269
+ panels directly. A table's sections and rows keep the attributes they have a
270
+ slot for. A code block resolves the no-break-space sentinel instead of
271
+ emitting it. A fence opened inside a
272
+ container keeps that container open, so a boundary line, a list marker at the
273
+ content column and a closed fence's residue land where PART 9 §24 puts them
274
+ rather than folding into the code text. A lazy line folded into a container
275
+ leaves it open. A caption attaches across at most one blank line, so two blank
276
+ lines detach it and the image stays an image. A definition body is an
277
+ indented-block collector, so a line below its column ends it. A heading with
278
+ no `<section>` wrapper renders the author's attributes first and the generated
279
+ id last. A frontmatter block whose opener named no format is written back as
280
+ `---yaml`, and a blank line inside a fenced block under a footnote definition
281
+ or a definition-list description is written empty rather than indented.
282
+
283
+ Reference resolution reaches three places it used to stop short of: a
284
+ reference inside an inline note, a critic insertion or a critic deletion
285
+ resolves against the document's definitions; a footnote inside an unresolved
286
+ reference stays a footnote rather than being swallowed by the failed
287
+ reference; and a reference tail no longer seals its own link text, so the
288
+ text a reference link carries survives the frame that resolves it.
289
+
290
+ For `Carve.parse` specifically: a nested link and an autolink stay nodes and
291
+ the renderers unwrap them; a collapsed reference publishes the label it
292
+ resolves by; a heading's derived display text clones the heading's nodes
293
+ instead of re-rendering them, so an escaped character in a heading reaches the
294
+ label; and `attrs.keyValues` is published in the author's source order, the
295
+ same order the sibling `attrs.order` field states.
296
+
297
+
10
298
  ## [0.1.0] - 2026-07-12
11
299
 
12
300
  ### Added
@@ -18,5 +306,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
306
  Arrays (every AST node type is covered), enabling custom renderers such as
19
307
  [carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
20
308
 
21
- [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...HEAD
309
+ [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...HEAD
310
+ [0.1.1]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...v0.1.1
22
311
  [0.1.0]: https://github.com/markup-carve/carve-rb/releases/tag/v0.1.0
data/README.md CHANGED
@@ -37,6 +37,14 @@ time via `rb_sys`.
37
37
  require "carve"
38
38
 
39
39
  Carve.to_html("# Hello *world*")
40
+
41
+ # Every core engine target is available from the binding.
42
+ Carve.to_markdown(source)
43
+ Carve.to_plain_text(source)
44
+ Carve.to_ansi(source)
45
+ Carve.to_carve(source)
46
+ Carve.from_html('<p>Hello <strong>world</strong></p>')
47
+ Carve.from_markdown('*em* and **strong**')
40
48
  # => "<section id=\"Hello-world\">\n <h1>Hello <strong>world</strong></h1>\n</section>"
41
49
 
42
50
  # Carve syntax note: *...* is STRONG (bold), /.../ is EMPHASIS (italic).
@@ -54,10 +62,18 @@ Carve.to_html(src, extensions: %w[math-block list-table])
54
62
 
55
63
  ### Recognized extensions
56
64
 
57
- `autolink`, `details`, `list_table`, `math_block`, `heading_permalinks`,
58
- `citations`, `code_callouts`, `tab_normalize`, `wikilinks`, `external_links`,
59
- `fenced_render`, `fenced_render_graphviz`, `fenced_render_chart`, `spoiler`,
60
- `table_of_contents` (see `Carve::EXTENSIONS`).
65
+ `Carve::EXTENSIONS` is the list, and it comes from the engine rather than from
66
+ a copy kept here that could fall behind it:
67
+
68
+ ```ruby
69
+ Carve::EXTENSIONS
70
+ # => [:autolink, :citations, :"code-callouts", :"code-group", ...]
71
+ ```
72
+
73
+ The canonical names are kebab-case (`:"math-block"`, `:"table-of-contents"`).
74
+ Snake_case spellings (`:math_block`) work as arguments, as do the short aliases
75
+ this binding has always taken: `:math`, `:permalinks`, `:mermaid`, `:dot`,
76
+ `:graphviz`, `:chart`, `:toc`.
61
77
 
62
78
  An unknown extension name raises `ArgumentError`.
63
79
 
@@ -131,6 +147,115 @@ carve-py #1).
131
147
 
132
148
  An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
133
149
 
150
+ ## Symbols
151
+
152
+ A `:name:` symbol renders its literal `:name:` source unless the name is in the
153
+ **symbols map** passed as `symbols:` (String or Symbol keys, String values):
154
+
155
+ ```ruby
156
+ Carve.to_html("Ship it :rocket: :shrug:", symbols: { "rocket" => "🚀" })
157
+ # => "<p>Ship it 🚀 :shrug:</p>" (an unmapped name stays literal)
158
+ ```
159
+
160
+ The leading word-boundary guard is unaffected by an active map: `a:b:c`,
161
+ `10:30:` and `me@example.com` never become symbols. A non-String value raises
162
+ `TypeError`.
163
+
164
+ > **Security: symbol values are TRUSTED RAW output.**
165
+ > A mapped value is inserted into the output **unescaped** - the same trust
166
+ > class as a `renderers:` callable. `{ "b" => "<b>x</b>" }` emits a real `<b>`
167
+ > element, not escaped text. This is deliberate (processor configuration is
168
+ > trusted). **Never build a symbols map out of untrusted / user-supplied
169
+ > input.**
170
+
171
+ ## Section wrappers
172
+
173
+ A top-level heading is wrapped, along with the content following it up to the
174
+ next same-or-shallower heading, in a `<section>` carrying the heading's id (spec
175
+ PART 9 §13). Only the id moves - `{#install .featured}` gives
176
+ `<section id="install"><h2 class="featured">` - and a heading inside a
177
+ blockquote, div or list item is not wrapped at all.
178
+
179
+ Pass `sections: false` to render headings flat, with the id back on the `<h*>`:
180
+
181
+ ```ruby
182
+ Carve.to_html("# A\n\np\n")
183
+ # => "<section id=\"A\">\n <h1>A</h1>\n <p>p</p>\n</section>"
184
+
185
+ Carve.to_html("# A\n\np\n", sections: false)
186
+ # => "<h1 id=\"A\">A</h1>\n<p>p</p>"
187
+ ```
188
+
189
+ This is for a host whose CSS or JS assumes rendered blocks are direct children
190
+ of the content container - the `.stack > * + *` spacing idiom, `:first-child`,
191
+ `nth-child()` counting, DOM child walks - all of which stop matching once a
192
+ wrapper sits in between. It is the one output change that breaks a document
193
+ whose *source* migrated cleanly.
194
+
195
+ Nothing else changes: ids, collision dedup, `</#id>` cross-references, implicit
196
+ `[Heading][]` references and heading numbering all resolve against the slug
197
+ rather than the element carrying it. The endnotes
198
+ `<section role="doc-endnotes">` is a separate construct and is still emitted.
199
+
200
+ ## Untrusted input
201
+
202
+ Carve's normative hardening is always on and needs no option: dangerous URL
203
+ schemes are blanked, event-handler attributes like `onclick` are dropped, and the
204
+ bidi override/isolate characters behind Trojan Source are removed from rendered
205
+ text.
206
+
207
+ Raw passthrough is the deliberate exception. A ` ```=html ` block or a
208
+ `` `…`{=html} `` span renders **verbatim** by design, so it is the one thing
209
+ input you did not author has to switch off:
210
+
211
+ ``` ruby
212
+ Carve.to_html(user_input, safe: true, profile: :comment)
213
+ ```
214
+
215
+ `safe:` escapes those raw blocks and spans instead of emitting them. `profile:`
216
+ restricts which constructs are allowed at all and caps input length -
217
+ `:full`, `:article`, `:comment` or `:minimal`, String or Symbol. An unknown name
218
+ raises `ArgumentError` rather than being ignored.
219
+
220
+ A profile **rejection** raises too, rather than returning something that looks
221
+ like output:
222
+
223
+ ``` ruby
224
+ Carve.to_html("x" * 20_000, profile: :minimal)
225
+ # ArgumentError: Profile violations: 'document' is not allowed: max_length_exceeded (...)
226
+ ```
227
+
228
+ That matters for untrusted input: the engine's infallible entry point answers a
229
+ rejection with an empty String, which a caller cannot tell from a document that
230
+ legitimately rendered to nothing.
231
+
232
+ Full recipe, defaults and threat model:
233
+ [Security](https://markup-carve.github.io/carve/security).
234
+
235
+ ## Stored documents and spec versions
236
+
237
+ `carve fmt --stamp` (in any Carve engine) records the spec version a document was
238
+ last processed under. This gem reads that marker back, so a repository of stored
239
+ `.crv` files can be checked for documents predating a breaking spec change:
240
+
241
+ ``` ruby
242
+ Carve.read_stamp(source)
243
+ # => {version: "0.1", generated_by: "carve-php 0.1.0"}
244
+
245
+ Carve.needs_review?(source) # true when the document predates this engine
246
+ ```
247
+
248
+ An **unstamped** document answers `true`: its provenance is unknown, and assuming
249
+ it is current is the unsafe direction. Both marker forms are read, and a marker
250
+ written by any engine reads the same - the format is the contract, not any one
251
+ API - so the answer matches carve-php, carve-js, carve-rs and carve-go on the
252
+ same document.
253
+
254
+ What a version difference means is the
255
+ [versioning contract](https://markup-carve.github.io/carve/versioning): only
256
+ `[behavior]` changelog entries between the stamped version and yours can require
257
+ a document change.
258
+
134
259
  ## API
135
260
 
136
261
  | Method | Description |
@@ -139,8 +264,14 @@ An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
139
264
  | `Carve.parse(source)` | Parse Carve source into an AST (tree of Ruby Hashes/Arrays). |
140
265
  | `Carve.to_html(source, extensions: [...])` | Render with the named extensions enabled. |
141
266
  | `Carve.to_html(source, mode: :static, renderers: {...})` | Render self-contained static HTML with build-time renderers. |
267
+ | `Carve.to_html(source, symbols: {...})` | Render with a `:name:` -> value symbol map (values are raw, see above). |
268
+ | `Carve.to_html(source, safe: true, profile: :comment)` | Render untrusted input: escape `=html` raw blocks/spans, restrict constructs. |
269
+ | `Carve.to_html(source, sections: false)` | Render headings flat, with the id on the `<h*>` instead of a `<section>` wrapper. |
270
+ | `Carve.read_stamp(source)` | Read a document's provenance marker: `{version:, generated_by:}` or `nil`. |
271
+ | `Carve.needs_review?(source)` | Whether a document predates this engine's spec version (unstamped counts as yes). |
142
272
  | `Carve.to_html_with_extensions(source, names_array)` | Native primitive (Array of Strings). |
143
273
  | `Carve.to_html_full(source, names_array, mode_string, renderers_hash)` | Native static-mode primitive. |
274
+ | `Carve.to_html_full_with_symbols(source, names_array, mode_string, renderers_hash, symbols_hash)` | Native primitive, static mode + symbol map. |
144
275
  | `Carve::VERSION` | Gem version. |
145
276
  | `Carve::EXTENSIONS` | Array of recognized extension symbols. |
146
277
  | `Carve::MODES` | Array of recognized render modes (`:interactive`, `:static`). |
@@ -171,15 +302,29 @@ rake test # runs the minitest suite
171
302
  builds:
172
303
 
173
304
  ```toml
174
- carve_rs = { package = "carve", git = "https://github.com/markup-carve/carve-rs", rev = "09c64118223c1574d136c308cfe2a70df8ab5128" }
305
+ carve_rs = { package = "carve-lang", git = "https://github.com/markup-carve/carve-rs", rev = "..." }
175
306
  ```
176
307
 
177
- The crate is imported under the alias `carve_rs`. Note: carve-rs has since
178
- renamed its published crate to `carve-lang` on `main`; this pin predates that
179
- rename, so `package = "carve"` is correct for this exact `rev`. When bumping to
180
- a newer carve-rs commit (past the rename), update both the `rev` and
181
- `package = "carve-lang"`.
308
+ Read the current revision out of `ext/carve/Cargo.toml` rather than from a copy
309
+ here. This section used to quote one, and it drifted three bumps behind the
310
+ manifest before anyone noticed - a duplicated value goes stale the first time
311
+ someone edits the original, and a stale one here is worse than none because it
312
+ reads as authoritative.
313
+
314
+ The crate is imported under the alias `carve_rs`. It is published as `carve-lang`
315
+ (carve-rs renamed it from `carve`), so a pin at any revision past that rename
316
+ needs `package = "carve-lang"` as above.
317
+
318
+ When bumping the `rev`, run `rake compile` and commit the resulting
319
+ `ext/carve/Cargo.lock` in the same change. The lock records the resolved
320
+ revision, so leaving it behind means every fresh clone gets a dirty working tree
321
+ on its first build and the gem can resolve to a different engine than the one
322
+ that was tested.
182
323
 
183
- ## License
324
+ Whether the pin is current is not a judgment call: CI runs the mandatory spec
325
+ corpus through the compiled extension and requires byte-identical HTML, so a pin
326
+ that has fallen behind fails a build. Locally:
184
327
 
185
- MIT, markup-carve.
328
+ ```sh
329
+ CARVE_SPEC_CORPUS=/path/to/carve/tests/corpus bundle exec rake test
330
+ ```