carve-lang 0.1.0 → 0.1.1

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: f75d9a093c48417037884f79d3372f6d8ba90ef5b60781789ec293e68f8a170c
4
+ data.tar.gz: 39418a3a8ab54c8b9886066a90d5aac6071bea72a6b70467fbf3e955032c6979
5
5
  SHA512:
6
- metadata.gz: b7c2b1b33a8a979ac4ba2f4b8986e7876188ebe8105c5cb9beb9d55e7f068a04f9bcd785b5837309c5fd408670994c57e3706e39979b36adf1a0c8cbb8226738
7
- data.tar.gz: a676302d1f1b343abf78e324bdd48dcedd5146e6637370ef3276f33cab5479c854d149f3b9365865867d1f008b1fe3af086b2201ac1b41e79dbb12c31a3f402c
6
+ metadata.gz: e9e3c6dddd06a979acc9c6bd100f711b5e32b741f885a82a873547dc72abba3b6b7c64a5861b1bc87cedd62ff85a45ed4a439b2084ab5f4d85deec7678d58112
7
+ data.tar.gz: e05bd66036ca360b3516161990b02e8e5f38ea140a52dc955fc55b082fc4f7da3d57d7bcdf46711aa4e765aa16682c6371f573ed241eb619434a2fbec0dcfc98
data/CHANGELOG.md CHANGED
@@ -7,6 +7,156 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2026-08-18
11
+
12
+ Everything below is the delta from `v0.1.0`, which is the only version a
13
+ reader can be upgrading from. The engine pin moved seven times inside this
14
+ window and lands on carve-rs `0.1.3` (`a33c42ad`); the intermediate revisions
15
+ are not listed, because no release ever shipped them.
16
+
17
+ ### Security
18
+
19
+ - **A list-valued URL attribute is probed at every candidate, not at its head**
20
+ (PART 9 §25, markup-carve/carve#1320). The sanitizer read only the value's
21
+ leading scheme, which vouches for the whole value only where the whole value
22
+ is one URL, so `srcset="safe.png 1x, javascript:alert(1) 2x"` passed on its
23
+ second entry. `srcset`, `imagesrcset`, `ping` and `attributionsrc` are now
24
+ split into tokens and every candidate is read, and any hit blanks the whole
25
+ value. The engine embedded in `0.1.0` predates the fix, so the gem published
26
+ on 2026-07-12 carries the defect.
27
+
28
+ ### Added
29
+
30
+ - **`Carve.parse` publishes source positions.** Every block node the engine can
31
+ place carries `pos` with `startLine`, `endLine`, `startColumn`, `endColumn`,
32
+ `startOffset` and `endOffset`, as PART 12 §4 spells it: lines and columns
33
+ 1-based, offsets 0-based, counted in Unicode codepoints, `endColumn` and
34
+ `endOffset` exclusive. A node whose span the engine could not determine has
35
+ no `pos` key at all rather than one holding placeholder numbers - an absent
36
+ position is a fact a consumer can act on, an invented one is not. Inline
37
+ nodes do not carry positions yet (markup-carve/carve-rs#333).
38
+
39
+ - **`Carve.to_html(source, sections: false)` renders headings without the
40
+ `<section>` wrapper** (#36, PART 9 §13). The id goes back on the `<h*>`
41
+ alongside its other attributes and the blocks that would have been section
42
+ children stay siblings. Default `true`, so existing output is unchanged. The
43
+ wrapper is the one output change that breaks a site whose source migrated
44
+ cleanly: CSS and JS assuming rendered blocks are direct children of the
45
+ content container stop matching once a `<section>` sits in between.
46
+
47
+ - **Every extension the engine registers is reachable from Ruby.**
48
+ `Carve::EXTENSIONS` lists 31 names instead of 15, adding glossary, index,
49
+ table-of-contents placement (`::: toc`), heading numbers, heading references,
50
+ heading level shift, code groups, tabs, the img fence, color swatches, smart
51
+ quotes, and the remaining fenced-render presets (PlantUML, D2, WaveDrom,
52
+ Vega-Lite, ABC). The list comes from the engine's registry rather than being
53
+ typed out in the native binding and again in `lib/carve.rb`. Canonical names
54
+ are the engine's kebab-case keys; snake_case spellings and the short aliases
55
+ (`:math`, `:permalinks`, `:mermaid`, `:dot`, `:chart`, `:toc`) still work.
56
+
57
+ - **Composite figures** (PART 9 §4c). A **bare** `::: figure` fence is no longer
58
+ a generic container: it is one figure of ordered panels, rendering
59
+ `<figure class="carve-figure-group">` around a
60
+ `<div class="carve-figure-panels">` whose members are each a
61
+ `<figure class="carve-figure-panel">`, and a `^ ` line following the closer
62
+ becomes the group's `<figcaption>` instead of an ordinary paragraph. A fence
63
+ carrying a title or a `[label]` keeps the old shape, so a document that named
64
+ its figure divs renders as before. `Carve.parse` publishes the group as a new
65
+ `figure_group` node type.
66
+
67
+ ### Changed
68
+
69
+ - **Breaking (`Carve.parse`): the published tree now matches the reference
70
+ shape.** A tree from this binding did not interoperate with carve-js or
71
+ carve-php, so several names and the root's shape moved at once:
72
+
73
+ | was | now |
74
+ |---|---|
75
+ | `critic_insert` / `critic_delete` / `critic_substitute` | `insert` / `delete` / `substitution` |
76
+ | `footnote` (both forms) | `footnote_ref` / `inline_footnote` |
77
+ | `ref_id` | `refId` |
78
+ | `footnote_defs` | `footnoteDefs` |
79
+ | `source_len` | `srcByteLength` |
80
+ | `emoji` | `symbol` (and it carries `attrs`) |
81
+
82
+ The root now carries exactly `type`, `children` and `srcByteLength`.
83
+ Frontmatter and footnote definitions are block nodes in `children` rather
84
+ than root fields, which PART 12 §7 requires: a root field cannot carry the
85
+ position §4 requires of every node, and both are source an editor navigates
86
+ to (carve#411, carve#418). Frontmatter is the first child, carrying `format`
87
+ and **raw** `content` - not parsed key/values, which could not represent a
88
+ `---toml` block at all. Anything reading `ast[:frontmatter]` or
89
+ `ast[:footnoteDefs]` breaks.
90
+
91
+ - **Breaking (`Carve.parse`): three constructs became their own node types.** A
92
+ backslash escape is `{"type":"escaped_text","value":"-"}` instead of being
93
+ folded into surrounding text - the backslash carries intent the character does
94
+ not, since an author writes `\-\-` precisely so a consumer will not render an
95
+ en dash (carve#350). A `::: |` fence is `line_block` instead of a `div` with a
96
+ `.line-block` class, because inside it every newline is a hard break and a
97
+ class alone could not say which one a node was (carve#359). A block
98
+ extension's `summary` is a list of inline nodes rather than a plain string,
99
+ matching the admonition `title` shape. Definition lists publish
100
+ `definition_term` and `definition_description` nodes
101
+ (markup-carve/carve-rs#374).
102
+
103
+ These are additive for a consumer with a default branch and a compile error
104
+ for one without.
105
+
106
+ - **Delimited inline comments carried in from the engine.** `{% … %}` is an
107
+ inline comment that ends at its closer, alongside the trailing `%%` form that
108
+ runs to the end of its inline run (PART 9 §21a). Both render nothing; in
109
+ `Carve.parse` they are the same `comment` node, told apart by a `delimited`
110
+ field, because the two spell different documents and a writer has to
111
+ reproduce the one that was written.
112
+
113
+ - **Language changes carried in from the engine.** Superscript and subscript are
114
+ braced-only - bare `^x^` and `,x,` are literal text, the markup is `{^x^}` and
115
+ `{,x,}`. The `:name:` symbol inline was added, with a leading word-boundary
116
+ guard so `a:b:c`, `10:30:` and `me@example.com` stay literal. Compact semantic
117
+ spans arrived: `[Tab]{kbd}` renders `<kbd>Tab</kbd>`, with `abbr`, `time` and
118
+ `kbd` in core and `samp`, `var`, `cite` and `dfn` behind the `semantic-span`
119
+ extension.
120
+
121
+ Every `<th>` carries a `scope` - `col` in the header run, `row` for a
122
+ `|=` cell in a body row. A cell's attribute block binds after its kind and
123
+ alignment markers, so a `{...}` following a cell's `=` marker is read as
124
+ attributes rather than rendered as cell text. A mandatory base class merges
125
+ into the author's class slot at its authored position rather than leading it,
126
+ so `:widget[x]{#i .c}` keeps `id` first.
127
+
128
+ - **Rendering corrections carried in from the engine.** A block-attribute line
129
+ reaches the nested list it was written for, and a flush-left attribute line is
130
+ read as attributes rather than as paragraph text. A figure group holds its
131
+ panels directly. A table's sections and rows keep the attributes they have a
132
+ slot for. A code block resolves the no-break-space sentinel instead of
133
+ emitting it. A fence opened inside a
134
+ container keeps that container open, so a boundary line, a list marker at the
135
+ content column and a closed fence's residue land where PART 9 §24 puts them
136
+ rather than folding into the code text. A lazy line folded into a container
137
+ leaves it open. A caption attaches across at most one blank line, so two blank
138
+ lines detach it and the image stays an image. A definition body is an
139
+ indented-block collector, so a line below its column ends it. A heading with
140
+ no `<section>` wrapper renders the author's attributes first and the generated
141
+ id last. A frontmatter block whose opener named no format is written back as
142
+ `---yaml`, and a blank line inside a fenced block under a footnote definition
143
+ or a definition-list description is written empty rather than indented.
144
+
145
+ Reference resolution reaches three places it used to stop short of: a
146
+ reference inside an inline note, a critic insertion or a critic deletion
147
+ resolves against the document's definitions; a footnote inside an unresolved
148
+ reference stays a footnote rather than being swallowed by the failed
149
+ reference; and a reference tail no longer seals its own link text, so the
150
+ text a reference link carries survives the frame that resolves it.
151
+
152
+ For `Carve.parse` specifically: a nested link and an autolink stay nodes and
153
+ the renderers unwrap them; a collapsed reference publishes the label it
154
+ resolves by; a heading's derived display text clones the heading's nodes
155
+ instead of re-rendering them, so an escaped character in a heading reaches the
156
+ label; and `attrs.keyValues` is published in the author's source order, the
157
+ same order the sibling `attrs.order` field states.
158
+
159
+
10
160
  ## [0.1.0] - 2026-07-12
11
161
 
12
162
  ### Added
@@ -18,5 +168,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
18
168
  Arrays (every AST node type is covered), enabling custom renderers such as
19
169
  [carve-hexapdf](https://github.com/markup-carve/carve-hexapdf).
20
170
 
21
- [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...HEAD
171
+ [Unreleased]: https://github.com/markup-carve/carve-rb/compare/v0.1.1...HEAD
172
+ [0.1.1]: https://github.com/markup-carve/carve-rb/compare/v0.1.0...v0.1.1
22
173
  [0.1.0]: https://github.com/markup-carve/carve-rb/releases/tag/v0.1.0
data/README.md CHANGED
@@ -54,10 +54,18 @@ Carve.to_html(src, extensions: %w[math-block list-table])
54
54
 
55
55
  ### Recognized extensions
56
56
 
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`).
57
+ `Carve::EXTENSIONS` is the list, and it comes from the engine rather than from
58
+ a copy kept here that could fall behind it:
59
+
60
+ ```ruby
61
+ Carve::EXTENSIONS
62
+ # => [:autolink, :citations, :"code-callouts", :"code-group", ...]
63
+ ```
64
+
65
+ The canonical names are kebab-case (`:"math-block"`, `:"table-of-contents"`).
66
+ Snake_case spellings (`:math_block`) work as arguments, as do the short aliases
67
+ this binding has always taken: `:math`, `:permalinks`, `:mermaid`, `:dot`,
68
+ `:graphviz`, `:chart`, `:toc`.
61
69
 
62
70
  An unknown extension name raises `ArgumentError`.
63
71
 
@@ -131,6 +139,115 @@ carve-py #1).
131
139
 
132
140
  An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
133
141
 
142
+ ## Symbols
143
+
144
+ A `:name:` symbol renders its literal `:name:` source unless the name is in the
145
+ **symbols map** passed as `symbols:` (String or Symbol keys, String values):
146
+
147
+ ```ruby
148
+ Carve.to_html("Ship it :rocket: :shrug:", symbols: { "rocket" => "🚀" })
149
+ # => "<p>Ship it 🚀 :shrug:</p>" (an unmapped name stays literal)
150
+ ```
151
+
152
+ The leading word-boundary guard is unaffected by an active map: `a:b:c`,
153
+ `10:30:` and `me@example.com` never become symbols. A non-String value raises
154
+ `TypeError`.
155
+
156
+ > **Security: symbol values are TRUSTED RAW output.**
157
+ > A mapped value is inserted into the output **unescaped** - the same trust
158
+ > class as a `renderers:` callable. `{ "b" => "<b>x</b>" }` emits a real `<b>`
159
+ > element, not escaped text. This is deliberate (processor configuration is
160
+ > trusted). **Never build a symbols map out of untrusted / user-supplied
161
+ > input.**
162
+
163
+ ## Section wrappers
164
+
165
+ A top-level heading is wrapped, along with the content following it up to the
166
+ next same-or-shallower heading, in a `<section>` carrying the heading's id (spec
167
+ PART 9 §13). Only the id moves - `{#install .featured}` gives
168
+ `<section id="install"><h2 class="featured">` - and a heading inside a
169
+ blockquote, div or list item is not wrapped at all.
170
+
171
+ Pass `sections: false` to render headings flat, with the id back on the `<h*>`:
172
+
173
+ ```ruby
174
+ Carve.to_html("# A\n\np\n")
175
+ # => "<section id=\"A\">\n <h1>A</h1>\n <p>p</p>\n</section>"
176
+
177
+ Carve.to_html("# A\n\np\n", sections: false)
178
+ # => "<h1 id=\"A\">A</h1>\n<p>p</p>"
179
+ ```
180
+
181
+ This is for a host whose CSS or JS assumes rendered blocks are direct children
182
+ of the content container - the `.stack > * + *` spacing idiom, `:first-child`,
183
+ `nth-child()` counting, DOM child walks - all of which stop matching once a
184
+ wrapper sits in between. It is the one output change that breaks a document
185
+ whose *source* migrated cleanly.
186
+
187
+ Nothing else changes: ids, collision dedup, `</#id>` cross-references, implicit
188
+ `[Heading][]` references and heading numbering all resolve against the slug
189
+ rather than the element carrying it. The endnotes
190
+ `<section role="doc-endnotes">` is a separate construct and is still emitted.
191
+
192
+ ## Untrusted input
193
+
194
+ Carve's normative hardening is always on and needs no option: dangerous URL
195
+ schemes are blanked, event-handler attributes like `onclick` are dropped, and the
196
+ bidi override/isolate characters behind Trojan Source are removed from rendered
197
+ text.
198
+
199
+ Raw passthrough is the deliberate exception. A ` ```=html ` block or a
200
+ `` `…`{=html} `` span renders **verbatim** by design, so it is the one thing
201
+ input you did not author has to switch off:
202
+
203
+ ``` ruby
204
+ Carve.to_html(user_input, safe: true, profile: :comment)
205
+ ```
206
+
207
+ `safe:` escapes those raw blocks and spans instead of emitting them. `profile:`
208
+ restricts which constructs are allowed at all and caps input length -
209
+ `:full`, `:article`, `:comment` or `:minimal`, String or Symbol. An unknown name
210
+ raises `ArgumentError` rather than being ignored.
211
+
212
+ A profile **rejection** raises too, rather than returning something that looks
213
+ like output:
214
+
215
+ ``` ruby
216
+ Carve.to_html("x" * 20_000, profile: :minimal)
217
+ # ArgumentError: Profile violations: 'document' is not allowed: max_length_exceeded (...)
218
+ ```
219
+
220
+ That matters for untrusted input: the engine's infallible entry point answers a
221
+ rejection with an empty String, which a caller cannot tell from a document that
222
+ legitimately rendered to nothing.
223
+
224
+ Full recipe, defaults and threat model:
225
+ [Security](https://markup-carve.github.io/carve/security).
226
+
227
+ ## Stored documents and spec versions
228
+
229
+ `carve fmt --stamp` (in any Carve engine) records the spec version a document was
230
+ last processed under. This gem reads that marker back, so a repository of stored
231
+ `.crv` files can be checked for documents predating a breaking spec change:
232
+
233
+ ``` ruby
234
+ Carve.read_stamp(source)
235
+ # => {version: "0.1", generated_by: "carve-php 0.1.0"}
236
+
237
+ Carve.needs_review?(source) # true when the document predates this engine
238
+ ```
239
+
240
+ An **unstamped** document answers `true`: its provenance is unknown, and assuming
241
+ it is current is the unsafe direction. Both marker forms are read, and a marker
242
+ written by any engine reads the same - the format is the contract, not any one
243
+ API - so the answer matches carve-php, carve-js, carve-rs and carve-go on the
244
+ same document.
245
+
246
+ What a version difference means is the
247
+ [versioning contract](https://markup-carve.github.io/carve/versioning): only
248
+ `[behavior]` changelog entries between the stamped version and yours can require
249
+ a document change.
250
+
134
251
  ## API
135
252
 
136
253
  | Method | Description |
@@ -139,8 +256,14 @@ An unknown `mode:` value or an unknown `renderers:` key raises `ArgumentError`.
139
256
  | `Carve.parse(source)` | Parse Carve source into an AST (tree of Ruby Hashes/Arrays). |
140
257
  | `Carve.to_html(source, extensions: [...])` | Render with the named extensions enabled. |
141
258
  | `Carve.to_html(source, mode: :static, renderers: {...})` | Render self-contained static HTML with build-time renderers. |
259
+ | `Carve.to_html(source, symbols: {...})` | Render with a `:name:` -> value symbol map (values are raw, see above). |
260
+ | `Carve.to_html(source, safe: true, profile: :comment)` | Render untrusted input: escape `=html` raw blocks/spans, restrict constructs. |
261
+ | `Carve.to_html(source, sections: false)` | Render headings flat, with the id on the `<h*>` instead of a `<section>` wrapper. |
262
+ | `Carve.read_stamp(source)` | Read a document's provenance marker: `{version:, generated_by:}` or `nil`. |
263
+ | `Carve.needs_review?(source)` | Whether a document predates this engine's spec version (unstamped counts as yes). |
142
264
  | `Carve.to_html_with_extensions(source, names_array)` | Native primitive (Array of Strings). |
143
265
  | `Carve.to_html_full(source, names_array, mode_string, renderers_hash)` | Native static-mode primitive. |
266
+ | `Carve.to_html_full_with_symbols(source, names_array, mode_string, renderers_hash, symbols_hash)` | Native primitive, static mode + symbol map. |
144
267
  | `Carve::VERSION` | Gem version. |
145
268
  | `Carve::EXTENSIONS` | Array of recognized extension symbols. |
146
269
  | `Carve::MODES` | Array of recognized render modes (`:interactive`, `:static`). |
@@ -171,14 +294,32 @@ rake test # runs the minitest suite
171
294
  builds:
172
295
 
173
296
  ```toml
174
- carve_rs = { package = "carve", git = "https://github.com/markup-carve/carve-rs", rev = "09c64118223c1574d136c308cfe2a70df8ab5128" }
297
+ carve_rs = { package = "carve-lang", git = "https://github.com/markup-carve/carve-rs", rev = "..." }
175
298
  ```
176
299
 
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"`.
300
+ Read the current revision out of `ext/carve/Cargo.toml` rather than from a copy
301
+ here. This section used to quote one, and it drifted three bumps behind the
302
+ manifest before anyone noticed - a duplicated value goes stale the first time
303
+ someone edits the original, and a stale one here is worse than none because it
304
+ reads as authoritative.
305
+
306
+ The crate is imported under the alias `carve_rs`. It is published as `carve-lang`
307
+ (carve-rs renamed it from `carve`), so a pin at any revision past that rename
308
+ needs `package = "carve-lang"` as above.
309
+
310
+ When bumping the `rev`, run `rake compile` and commit the resulting
311
+ `ext/carve/Cargo.lock` in the same change. The lock records the resolved
312
+ revision, so leaving it behind means every fresh clone gets a dirty working tree
313
+ on its first build and the gem can resolve to a different engine than the one
314
+ that was tested.
315
+
316
+ Whether the pin is current is not a judgment call: CI runs the mandatory spec
317
+ corpus through the compiled extension and requires byte-identical HTML, so a pin
318
+ that has fallen behind fails a build. Locally:
319
+
320
+ ```sh
321
+ CARVE_SPEC_CORPUS=/path/to/carve/tests/corpus bundle exec rake test
322
+ ```
182
323
 
183
324
  ## License
184
325