@markup-carve/pandoc-carve 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,17 @@
1
1
  # pandoc-carve
2
2
 
3
- Bidirectional [Carve](https://github.com/markup-carve/carve) ↔ Pandoc bridge.
3
+ Bidirectional [Carve](https://github.com/markup-carve/carve) and Pandoc bridge.
4
+ It exports Carve through Pandoc's writers and imports any format handled by a
5
+ Pandoc reader.
4
6
 
5
- **Export:** converts Carve markup to Pandoc's JSON AST, unlocking every pandoc
6
- output format for Carve documents: LaTeX, Typst, DOCX, PDF, RST, JATS, EPUB,
7
- and dozens more. **Import:** converts anything pandoc reads (DOCX, LaTeX, RST,
8
- Org, MediaWiki, HTML, Markdown, ...) into Carve source.
9
-
10
- ```
11
- .crv ──parse──▶ Carve AST ──carveToPandoc()──▶ Pandoc JSON ──pandoc -f json -t X──▶ .tex / .typ / .docx / …
12
- .docx / .tex / … ──pandoc -t json──▶ Pandoc JSON ──pandocToCarve()──▶ Carve AST ──renderCarve──▶ .crv
7
+ ```text
8
+ .crv -> Carve AST -> Pandoc JSON -> LaTeX, Typst, DOCX, PDF, EPUB, ...
9
+ DOCX, LaTeX, RST, ... -> Pandoc JSON -> Carve AST -> .crv
13
10
  ```
14
11
 
15
- The bridge maps the parsed Carve AST node-by-node, so emphasis, admonitions,
16
- tables with spans, footnotes, math, and raw passthrough all arrive correctly.
17
- It also makes Carve's target-routed raw spans finally fire:
18
- `` `\alpha`{=latex} `` becomes a Pandoc `RawInline` that the LaTeX writer emits
19
- and every other writer drops - exactly the pandoc-Markdown semantics the syntax
20
- was born from.
12
+ The bridge maps parsed nodes instead of converting through HTML. It preserves
13
+ structure such as tables with spans, footnotes, math, admonitions, attributes,
14
+ and target-routed raw content when Pandoc can represent it.
21
15
 
22
16
  ## Install
23
17
 
@@ -25,368 +19,106 @@ was born from.
25
19
  npm install @markup-carve/pandoc-carve
26
20
  ```
27
21
 
28
- The CLI shells out to a `pandoc` executable on your PATH (3.x; tested against
29
- 3.10.2). Pandoc is not bundled - install it from [pandoc.org](https://pandoc.org)
30
- or your package manager. Emitting plain JSON (`-t json`) needs no pandoc at all.
22
+ The CLI requires Pandoc 3.x on `PATH` for formats other than JSON. CI currently
23
+ tests Pandoc 3.11 and 3.10.2. Pandoc is not bundled.
31
24
 
32
25
  ## CLI
33
26
 
34
27
  ```bash
35
- # Carve -> LaTeX
28
+ # Export
36
29
  pandoc-carve doc.crv -t latex -o doc.tex
37
-
38
- # Carve -> Typst, PDF, DOCX ... any pandoc writer
39
30
  pandoc-carve doc.crv -t typst -o doc.typ
40
- pandoc-carve doc.crv -t pdf -o doc.pdf
41
31
  pandoc-carve doc.crv -t docx -o doc.docx
32
+ pandoc-carve doc.crv -t pdf -o doc.pdf
42
33
 
43
- # Standalone document (pandoc templates)
44
- pandoc-carve doc.crv -t latex -s -o doc.tex
45
-
46
- # Just the Pandoc JSON AST (no pandoc needed)
47
- pandoc-carve doc.crv -t json
48
-
49
- # Expand contained includes; named files default to their own directory
50
- pandoc-carve book/main.crv -t json --include-root "$PWD/book"
51
-
52
- # Keep directives literal for a named file
53
- pandoc-carve book/main.crv -t json --no-includes
54
-
55
- # Read from stdin, pass extra args through to pandoc after --
56
- cat doc.crv | pandoc-carve - -t latex -- --toc
57
-
58
- # IMPORT: anything pandoc reads -> Carve
34
+ # Import
59
35
  pandoc-carve report.docx -f docx -o report.crv
60
36
  pandoc-carve paper.tex -f latex -o paper.crv
61
- pandoc-carve README.md -f markdown -o README.crv
62
37
 
63
- # Import from a pre-made pandoc JSON AST (no pandoc needed)
38
+ # Emit or consume Pandoc JSON without invoking pandoc
39
+ pandoc-carve doc.crv -t json
64
40
  pandoc -f rst -t json doc.rst | pandoc-carve - -f json
65
-
66
- # From a SERIALIZED Carve AST (spec PART 12) - from any engine, not just carve-js
67
- carve doc.crv --to-json | pandoc-carve - -f carve-json -t latex
68
- ```
69
-
70
- Anything Carve cannot map faithfully is reported on stderr as a
71
- `pandoc-carve: degraded ...` warning - nothing degrades silently. Unresolved or
72
- refused includes are reported as dropped, while safe include renames and heading
73
- clamps are normalized. For migration
74
- automation, `--diagnostics report.json` writes the same findings in a versioned
75
- `{ schemaVersion, sourceFormat, diagnostics }` JSON envelope without mixing
76
- them into document output. `--fail-on-loss` exits with code 3 for degraded or
77
- dropped findings, while preserved and normalized findings do not fail CI.
78
- Imports through a Pandoc reader other than `-f json` also exit
79
- 3 because fidelity before the Pandoc JSON boundary cannot be verified; stderr
80
- names that worst-case gate explicitly. Use `--diagnostics -` for JSON on stderr.
81
-
82
- ## API
83
-
84
- ```js
85
- import { carveToPandoc, carveToPandocJson } from '@markup-carve/pandoc-carve';
86
-
87
- const { doc, warnings, diagnostics, report } = carveToPandoc('Hello /world/!');
88
- // doc = { 'pandoc-api-version': [1, 23, 1], meta: {...}, blocks: [...] }
89
- // warnings = ['degraded: ...'] for lossy constructs
90
- // diagnostics = [{ code, direction, class, severity, fidelity, confidence, message, ... }]
91
- // report = { schemaVersion: 2, sourceFormat: 'carve', diagnostics }
92
-
93
- const json = carveToPandocJson('Hello /world/!'); // stringified doc
94
- ```
95
-
96
- String conversion leaves `{{ path }}` directives literal. File-backed hosts can
97
- opt into contained expansion and receive warnings and dependency identities:
98
-
99
- ```js
100
- import { readFileSync } from 'node:fs';
101
- import { carveToPandocWithIncludes } from '@markup-carve/pandoc-carve/node';
102
-
103
- const sourcePath = '/srv/book/main.crv';
104
- const result = carveToPandocWithIncludes(readFileSync(sourcePath, 'utf8'), {
105
- includeRoot: '/srv/book',
106
- sourcePath,
107
- });
108
- // includeWarnings and suppressedIncludeWarnings describe expansion diagnostics;
109
- // dependencies contains resolved and unresolved include identities.
110
41
  ```
111
42
 
112
- Both API paths must be absolute, and `sourcePath` must stay inside
113
- `includeRoot`. The CLI accepts ordinary relative input paths but requires an
114
- absolute `--include-root`. Stdin stays literal unless that option is supplied.
115
-
116
- What the converter reads is the **serialized AST of the Carve spec's PART 12**
117
- (the shape `resources/ast-schema.json` pins), not any implementation's runtime
118
- tree - so a document another engine already parsed converts the same way,
119
- whether it arrives as an object or as JSON text:
43
+ Anything that cannot be mapped faithfully produces a diagnostic. Use
44
+ `--diagnostics report.json` for the versioned JSON report and `--fail-on-loss`
45
+ to return exit code 3 when a conversion degrades or drops content. Imports
46
+ through a Pandoc reader other than `-f json` also return 3 because fidelity
47
+ before the Pandoc JSON boundary cannot be verified.
120
48
 
121
- ```js
122
- import { carveAstToPandoc, carveToCarveAst } from '@markup-carve/pandoc-carve';
49
+ Named input files expand contained includes by default, rooted at their own
50
+ directory. Use `--no-includes` to keep directives literal. Standard input
51
+ expands includes only when `--include-root` is supplied.
123
52
 
124
- // A tree from carve-rs, carve-php, carve-go ... or `carve doc.crv --to-json`
125
- const { doc, warnings, diagnostics } = carveAstToPandoc(serializedAstJson);
53
+ The [CLI reference](docs/reference.md#cli) covers standalone documents,
54
+ additional Pandoc arguments, includes, serialized Carve AST input, and every
55
+ diagnostic option.
126
56
 
127
- // The same exchange format, produced from source here
128
- const ast = carveToCarveAst('Hello /world/!');
129
- ```
130
-
131
- The reverse direction takes a Pandoc document (object or JSON string) and
132
- returns Carve source, serialized by carve's own `renderCarve` (the `carve fmt`
133
- serializer), so output formatting carries fmt's guarantees:
57
+ ## API
134
58
 
135
59
  ```js
136
- import { pandocToCarve, pandocToCarveAst } from '@markup-carve/pandoc-carve';
137
-
138
- const { carve, warnings, diagnostics, report } = pandocToCarve(pandocJsonString);
139
-
140
- // Preserve structured fields that Carve 0.1 source cannot spell, including
141
- // Pandoc's optional short figure/table caption.
142
- const { ast, warnings: astWarnings } = pandocToCarveAst(pandocJsonString);
143
- ```
144
-
145
- Round-trips are tested as a hard gate: `carve -> pandoc AST -> carve` must
146
- render byte-identical HTML to the original source across the test corpus.
147
- For exact restoration of attribute placement (`{.lead}` on a paragraph or
148
- list), export with `carveToPandoc(src, { roundtrip: true })` or the CLI's
149
- `--roundtrip` flag. In addition to the existing `carve-block` attribute marker,
150
- this preserves comments, typed citation fields, and unknown future Carve nodes
151
- with a versioned private Span/Div envelope. The native citation and readable
152
- unknown-node fallback remain usable by Pandoc. The marker may be visible in
153
- writer output and exact preservation is guaranteed only through Pandoc JSON.
154
- The complete format, safety checks, available source information, and filter
155
- semantics are documented in [the provenance envelope](docs/roundtrip-provenance.md).
156
-
157
- Pipe the JSON to pandoc yourself:
158
-
159
- ```bash
160
- node -e "import('@markup-carve/pandoc-carve').then(m => process.stdout.write(m.carveToPandocJson(require('fs').readFileSync('doc.crv','utf8'))))" \
161
- | pandoc -f json -t latex
60
+ import {
61
+ carveToPandoc,
62
+ carveToPandocJson,
63
+ pandocToCarve,
64
+ } from '@markup-carve/pandoc-carve'
65
+
66
+ const exported = carveToPandoc('Hello /world/!')
67
+ const json = carveToPandocJson('Hello /world/!')
68
+ const imported = pandocToCarve(json)
162
69
  ```
163
70
 
164
- ## What maps to what
71
+ Each direction returns structured diagnostics alongside its output. Node.js
72
+ hosts can use `carveToPandocWithIncludes` from the `/node` entry point for
73
+ contained file expansion and dependency tracking.
165
74
 
166
- | Carve | Pandoc |
167
- |-------|--------|
168
- | `/italic/`, `*bold*`, `/*both*/` | Emph, Strong, Strong+Emph |
169
- | `_underline_`, `~strike~`, `=highlight=` | Underline, Strikeout, Span `.mark` |
170
- | `{^sup^}`, `{,sub,}` | Superscript, Subscript |
171
- | Headings + `{#id .class}` attributes | Header with Attr |
172
- | Tables incl. rowspan/colspan and captions | Table (spans inverted to pandoc's origin-cell model) |
173
- | `table.rowGroups` counts (PART 12 section 15) | TableHead, one TableBody per group with its RowHeadColumns and intermediate header rows, TableFoot |
174
- | Cell attributes `\|{#id .cls k=v} text`, row attributes `\| a \|{.cls}` | the `Attr` pandoc's Cell and Row already carry |
175
- | Footnotes (reference and inline `^[..]`) | Note |
176
- | Math `` $`..` `` / `` $$`..` `` | Math Inline / Display |
177
- | Images/quotes with `^ caption` lines | Figure |
178
- | A bare `::: figure` composite (PART 9 section 4c) | Figure of nested Figures - pandoc's subfigure model - with the group caption on the outer one. An opener carrying a title or a `[label]` is not this production and stays a Div |
179
- | `::: note` admonitions | Div `.admonition .note` (+ title paragraph) |
180
- | Tabs / code-group panels, grouping `[label]` | Div; each `[label]` becomes a bold caption so panels stay distinguishable (graceful degradation) |
181
- | `` `x`{=latex} `` / ```` ```=latex ```` | RawInline / RawBlock (target-routed by pandoc) |
182
- | Citations `[@key]`, `[+@key]`, `[-@key, p. 33]` | Cite with one Citation per key (AuthorInText / SuppressAuthor / NormalCitation), the locator in the citation suffix, the verbatim source as the Cite content |
183
- | `@mention`, `#tag`, `:ext[..]`, critic markup | classed Spans (documented degradation) |
184
- | `[text]{.smallcaps}` | SmallCaps (pandoc's own class convention, both directions) |
185
- | Frontmatter, nested: maps, block and flow sequences, sequences of maps | Meta, to the depth pandoc's own reader gives it |
186
- | `::: \|` line blocks (verse) | LineBlock, one entry per line, an empty entry per stanza break |
187
- | Ordered markers `1.` / `1)` / `a.` / `iv.` | OrderedList with the matching style and delimiter. Pandoc's example list `(@)` and its `(1)` marker have no Carve form and are reported |
188
- | A no-break space the parser resolved (`\ `) | U+00A0 (the engines publish a private-use sentinel for it) |
189
- | A reference link, image or footnote nothing defines (`[r][]`, `[^f]`) | the literal source as text, which is what Carve renders, plus a warning naming the label |
75
+ Use `carveToCarveAst` to serialize source, `carveAstToPandoc` to convert a
76
+ serialized Carve tree, and `pandocToCarveAst` for the reverse direction. These
77
+ functions consume the versioned AST defined by the Carve specification, not an
78
+ implementation-specific runtime tree.
190
79
 
191
- The complete node-by-node contract lives in the test goldens. Worked
192
- input/output pairs in both directions - including how interactive constructs
193
- degrade for print formats - are in [`examples/`](examples/README.md).
80
+ ## Round trips
194
81
 
195
- ## Dangerous URL schemes are blanked
196
-
197
- A link or image destination whose scheme Carve's spec denies (PART 9 §25 -
198
- `javascript:`, `vbscript:`, `data:`, `file:`, and the OS protocol-handler class
199
- such as `ms-msdt:`, `search-ms:`, `shell:`, `vscode:` and `jar:`) leaves this
200
- bridge as an **empty target**, and an `unsafe-url-scheme` diagnostic is emitted
201
- naming the scheme and the destination that was refused.
202
-
203
- Carve's own HTML, Markdown and ANSI writers all blank it, and the clause binds
204
- every target that emits a resolvable URL. Pandoc's targets are not HTML, but
205
- `pandoc -f json -t html` is one command away - passing the scheme through here
206
- would not be a narrower policy, it would be the same sink one step removed.
207
-
208
- The diagnostic class is `lossy`, so `--fail-on-loss` stops on its dropped
209
- fidelity and a caller reading
210
- `diagnostics` can act on it. The scheme list and the scheme probe are mirrored
211
- from the engine rather than invented, and a test drives the engine's own writer
212
- over both to keep the mirror honest - the probe strips control characters and
213
- Unicode whitespace before it reads a scheme, so `<U+202F>javascript:` and
214
- `java<DEL>script:` are caught along with the plain spelling.
215
-
216
- Nothing else is touched. An `https:`, `mailto:`, `tel:`, `ftp:`, relative or
217
- fragment destination reaches pandoc exactly as written, and emits no
218
- diagnostic.
219
-
220
- > [!NOTE]
221
- > This covers link and image **destinations**. An `{onclick=…}`, `{style=…}` or
222
- > `{srcdoc=…}` attribute, which Carve's HTML writer also refuses, still reaches
223
- > pandoc - see [#163](https://github.com/markup-carve/pandoc-carve/issues/163).
224
-
225
- ## Why a bridge, not a pandoc reader?
226
-
227
- A native `Text.Pandoc.Readers.Carve` upstream would be a fourth full Carve
228
- parser, written in Haskell, outside the conformance loop that keeps the three
229
- official implementations byte-identical against the shared corpus. While the
230
- spec is on its 0.x line and still moving, that reader would drift on pandoc's
231
- release cadence and ship stale behavior. The bridge reuses the canonical,
232
- conformance-tested `@markup-carve/carve` parser instead - correct by
233
- construction, and it iterates in lockstep with the spec.
234
-
235
- A pandoc custom Lua reader has the same drift problem (it would reimplement
236
- the parser in Lua). Once the spec reaches 1.0 and stabilizes, contributing an
237
- upstream reader becomes attractive - with this bridge's node map and the
238
- conformance corpus as the oracle any port has to pass.
239
-
240
- ## Options
82
+ With round-trip metadata enabled, the test suite requires
83
+ `carve -> Pandoc AST -> carve` to render equivalent normalized HTML across the
84
+ corpus apart from an explicit known-lossy list:
241
85
 
242
86
  ```js
243
- carveToPandoc(src, {
244
- roundtrip: true, // stamp attr wrappers for exact re-import
245
- symbols: { heart: '♥' }, // resolve :name: symbols like the renderer would
246
- listTable: false, // opt OUT: keep ::: list-table as a degraded div
247
- citations: false, // opt OUT: read [@key] as an @mention
248
- extensions: [], // extra Carve extensions for the parse
249
- });
87
+ const result = carveToPandoc(source, { roundtrip: true })
250
88
  ```
251
89
 
252
- CLI equivalents: `--roundtrip`, `--symbols map.json`, `--no-list-table`,
253
- `--no-citations`.
254
-
255
- `listTable` and `citations` default ON, because the reverse direction writes
256
- both unprompted and a construct this bridge chooses on the way out has to be
257
- one it recognizes on the way back. With `citations` off the parser reaches the
258
- `@` first and `[@doe1990]` is a `.mention` span, so an imported bibliography
259
- becomes mentions; with `listTable` off a table this bridge itself wrote as
260
- `::: list-table` returns as a `Div` of nested lists. Turn them off to get what
261
- a processor with neither extension enabled would render.
262
-
263
- ## Limitations
264
-
265
- - A `table.rowGroups` partition whose counts do not add up to the table's row
266
- count is refused with a warning and the table converts with the implicit
267
- head/body split instead. PART 12 section 15 requires the sum as a MUST, and
268
- JSON Schema cannot express a cross-field sum, so a document that validates
269
- against `resources/ast-schema.json` can still be incoherent - the bridge
270
- checks it itself rather than trusting a green validator.
271
- - A pandoc table with block content in a cell (a list, two paragraphs, a code
272
- block) is imported as `::: list-table` rather than a pipe table, and that is
273
- now the ONLY reason a table leaves the pipe form. PART 9 §16's pipe-table cell
274
- holds inlines, so there is no pipe form for it, and the extension's cells are
275
- list items that hold full blocks. Structure is preserved; what the extension
276
- cannot spell is reported instead - a body group's attributes, a body boundary
277
- no header row marks, and body groups that disagree on their row-head column
278
- count. It converts back to the pandoc table by default; `listTable: false`
279
- returns the degraded div instead.
280
- - A pandoc table with ROW-HEAD COLUMNS stays a pipe table. `RowHeadColumns` says
281
- the leading N cells of every body row are row headers, and a pipe table says
282
- that per cell: `|= Mercury | 4,879.4 |` is a `<th scope="row">`. Marking the
283
- cells needs no extension and round-trips, because the export direction derives
284
- the count back from the leading run.
285
- - On the way out, one TableBody per RUN of body rows that agree on that leading
286
- run. Pandoc's `RowHeadColumns` is a count on a body and a `Table` holds a list
287
- of bodies, so rows that disagree are simply different bodies. A table whose
288
- body rows all agree emits exactly one body, as before.
289
- - Pandoc `SmallCaps` has no Carve node and is not getting one: it imports as a
290
- `[text]{.smallcaps}` span, with a warning saying so. The span is not a dead
291
- end, though - the export direction reads that class back as `SmallCaps`, the
292
- same convention pandoc's own markdown reader uses, so small caps survive
293
- Pandoc -> Carve -> Pandoc and still reach the LaTeX, Typst and DOCX writers.
294
- Other attributes on the span are preserved around it, exactly as pandoc does.
295
- - Pandoc `Quoted` imports as literal curly quote characters (`“…”` / `‘…’`).
296
- Carve has no quote node, and the characters are what an author would have
297
- typed. This one is genuinely one-way: the text re-exports as `Str`, so the
298
- quote kind and pandoc's locale-aware quoting are gone. Reported once per
299
- document.
300
- - A `ColSpec`'s `ColWidth` is dropped. Carve's table model has no width slot at
301
- any level and pipe-table source cannot spell one, so there is nowhere to put
302
- the number and no syntax that would reproduce it - a wontfix rather than a
303
- gap. The drop is silent on purpose: pandoc derives a `ColWidth` for every
304
- grid and multiline table from the ASCII column widths, so a diagnostic would
305
- fire on the ordinary case and report a value the author never chose. Column
306
- ALIGNMENT is carried in both directions; tables leaving the bridge always
307
- carry `ColWidthDefault`, which pandoc's writers size themselves.
308
- - A rowspan that starts in a header row and continues into the body is clipped
309
- to an empty body cell, with a warning. Carve is the richer model here: its
310
- rows are one flat list, while pandoc's `TableHead` and `TableBody` hold
311
- separate row lists and confine a cell's `rowSpan` to its own section. Moving
312
- the head/body boundary to make the span fit would silently reclassify a row,
313
- and duplicating the origin's content would invent a cell the author never
314
- wrote, so the grid keeps its shape and the diagnostic reports the loss.
315
- - Reverse conversion keeps flattening for display targets: `pandocToCarve`
316
- serializes through Carve 0.1 source, which has no spelling for row groups or
317
- a short caption, so those fields survive only on the `pandocToCarveAst` path.
318
- - Citations cross as pandoc's own citeproc convention, which is lossy in one
319
- respect: pandoc's `Citation` has no locator field, so Carve's typed
320
- `locatorLabel`/`locatorValue` pair is serialized into `citationSuffix` behind
321
- a `, ` and citeproc re-derives it from there. The bridge does not rebuild
322
- those two fields on the way back - the label table is section 4.2's, it lives
323
- in the engine, and a second copy here would drift. The locator TEXT round-
324
- trips byte for byte, so re-parsing the emitted source with the citations
325
- extension restores the typed pair. With `roundtrip: true`, the private
326
- provenance wrapper retains the original typed fields directly while its
327
- enclosed native `Cite` remains available to citeproc. A group whose items mix `AuthorInText` and
328
- `NormalCitation` cannot be spelled in Carve (the integral `+` is a property of
329
- the whole cluster) and is imported as integral with a warning.
330
- - Pandoc keeps its bibliography in document metadata, not in the AST, so
331
- importing a `Cite` emits no `[@key]:` definition lines. The citation itself
332
- round-trips: `citationNoteNum` is reproduced the way pandoc's own markdown
333
- reader computes it (notes closed so far, plus one), so a `Cite` returns
334
- unchanged rather than unchanged-except-one-integer.
335
- - Block content inside metadata (pandoc's `MetaBlocks`, e.g. `abstract: |`)
336
- round-trips as a YAML literal block scalar, the same spelling pandoc's own
337
- markdown writer emits and its reader turns back into `MetaBlocks` - by the
338
- FORM, so any key can carry it, not just `abstract`. The scalar keeps every
339
- line and every blank line between them, so the paragraph structure survives
340
- rather than flattening into one string. Every other `Meta` shape - maps,
341
- lists, lists of maps, scalars, booleans - round-trips too.
342
- - A `rowGroups` partition (a foot, several body groups, a body's own header
343
- rows or row-head columns) survives into the exchange AST, so
344
- `pandocToCarveAst` hands it on whole. `pandocToCarve` spells most of it too: a
345
- pipe table states its head and foot row counts on its attribute line
346
- (`{header-rows=2 footer-rows=1}`) and marks its row headers on the cells, so
347
- what flattens into body rows is a second body group and a body's own
348
- intermediate header rows and attributes - reported, not dropped quietly. Use
349
- the AST entry point when a second body group has to survive.
350
- - Stating the foot states the whole partition, and a stated partition is ONE
351
- body. A body carries one `RowHeadColumns`, so when the body rows disagree on
352
- how many leading cells are row headers, a table with a foot cannot keep them:
353
- the reader takes a count every row agrees on, and the disagreement is reported
354
- from both directions. Without a foot the reader splits a body at every change
355
- and every run survives.
356
- - A marker on a HEAD cell (`|=> Name |`) is the column's alignment and becomes
357
- pandoc's `ColSpec`; a marker on a BODY cell (`|> 12 |`) aligns that cell alone
358
- and becomes the cell's own `Alignment`. The two are not interchangeable: a
359
- pandoc cell with `AlignDefault` inherits its ColSpec, so promoting a body
360
- cell's marker to the column would align cells the author did not.
361
- - Column alignment needs a header row to live on (`|=> Name |`). A headerless
362
- pandoc table's column alignment is written onto each cell instead, which
363
- renders the same; the move is reported.
364
- - The frontmatter reader covers the YAML subset frontmatter uses, not YAML.
365
- Anchors, tags, multi-document streams, block scalars and flow maps are out; a
366
- line that fits no shape is reported and skipped rather than guessed at.
367
- - Scalars are typed the way pandoc's own frontmatter reader types them: an
368
- unquoted `true`, `yes`, `on` or `y` (and their negatives) is a `MetaBool`, a
369
- quoted `"true"` stays text, a null scalar keeps its key as an empty
370
- `MetaString`, and a number stays text because `Meta` has no number type. This
371
- matters beyond tidiness - a boolean read as text is not a near miss but an
372
- inversion, since a pandoc template testing `draft` sees any non-empty string
373
- as true.
374
- - Attribute order inside `{...}` is normalized to `#id .class key=val` on
375
- round-trip - pandoc's Attr has fixed slots, so the author's original order
376
- is not representable. Semantics are unchanged.
377
- - Import fidelity is bounded by `renderCarve` (carve fmt): the bridge hands it
378
- a byte-exact AST, but known fmt issues (e.g. trailing whitespace inside code
379
- blocks, carve-js issue 340) surface in the serialized output.
380
- - The pinned engine bounds what the source path can produce. The current git
381
- pin exports `toAstJson`, so the engine's own exchange serializer is used;
382
- with an older engine (any published release up to `0.1.3`),
383
- `src/ast-json.ts` performs the PART 12 section 7 mapping instead, and a pin
384
- that exports `toAstJson` takes over automatically.
385
- - Tier-3 visual extensions (mermaid, chart, code-group) arrive as their
386
- degraded block forms (code blocks / divs), same as Carve's static mode.
387
- `list-table` is the exception: it converts to a real Pandoc table with full
388
- block content per cell, unless `listTable: false` / `--no-list-table`.
90
+ The CLI equivalent is `--roundtrip`. Private Span and Div metadata preserves
91
+ comments, attribute placement, typed citations, and future Carve nodes through
92
+ Pandoc JSON. Other writers may expose or discard that metadata. See the
93
+ [provenance envelope](docs/roundtrip-provenance.md) for its format and safety
94
+ rules.
95
+
96
+ ## Fidelity and limitations
97
+
98
+ Pandoc's AST does not represent every Carve construct, and output formats have
99
+ their own limits. The bridge reports those cases rather than silently dropping
100
+ them. Highlights include:
101
+
102
+ - underline, highlight, and custom attributes depend on target-format support;
103
+ - complex Carve table structure may be normalized by a writer;
104
+ - grouped UI constructs such as tabs degrade to labeled blocks;
105
+ - raw target content only survives in matching writers;
106
+ - comments and attribute placement require round-trip metadata.
107
+
108
+ These limits are measured rather than asserted.
109
+ [pandoc-format-fidelity](https://github.com/markup-carve/pandoc-format-fidelity) runs Carve
110
+ source through this bridge and back for every writer it lists, and publishes the verdicts per
111
+ format: `results/carve-rt.json` holds the Carve round-trip lane, and
112
+ [the report](https://github.com/markup-carve/pandoc-format-fidelity/blob/main/docs/index.html)
113
+ covers every pandoc writer and reader.
114
+
115
+ Dangerous URL schemes such as `javascript:` are blanked and reported. The
116
+ [URL policy](docs/reference.md#dangerous-url-schemes-are-blanked) documents the
117
+ exact behavior. The [mapping and limitations reference](docs/reference.md#what-maps-to-what)
118
+ contains the node table and per-format limits; the [options section](docs/reference.md#options)
119
+ covers API configuration.
389
120
 
390
121
  ## Development
391
122
 
392
- Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
123
+ Contributor setup, tests, and maintenance commands are in the
124
+ [development guide](docs/development.md).
package/dist/ast-json.js CHANGED
@@ -38,6 +38,7 @@
38
38
  */
39
39
  const CHILD_FIELDS = [
40
40
  'children',
41
+ 'blocks',
41
42
  'items',
42
43
  'rows',
43
44
  'cells',
@@ -60,6 +61,13 @@ function isObject(value) {
60
61
  * object, so the runtime tree the caller still holds is never copied and never
61
62
  * mutated.
62
63
  */
64
+ /**
65
+ * The six kinds a `:::` container places rather than presents. They are
66
+ * `directive` nodes, not admonitions (carve#2195, carve#2225): a table of
67
+ * contents is not a callout. Releases up to 0.1.7 spell them `admonition`,
68
+ * and the schema refuses that `kind` there.
69
+ */
70
+ const DIRECTIVE_KINDS = new Set(['bibliography', 'footnotes', 'glossary', 'index', 'references', 'toc']);
63
71
  function normalize(node) {
64
72
  if (Array.isArray(node)) {
65
73
  let changed = false;
@@ -152,6 +160,20 @@ function normalize(node) {
152
160
  default:
153
161
  break;
154
162
  }
163
+ // A reference's label is spelled `label` on the wire, like the `footnote`
164
+ // definition it matches (carve#2213). Releases up to 0.1.7 serialize `id`,
165
+ // which the schema refuses - it sanctions decoding the old property onto
166
+ // the field (PART 12 section 11), which is what this does. Remove it once
167
+ // no supported release emits `id`. After the switch, because the pre-split
168
+ // fold above reaches `footnote_ref` by changing `type` rather than matching it.
169
+ const settled = out ?? node;
170
+ if (settled['type'] === 'admonition' && DIRECTIVE_KINDS.has(String(settled['kind']))) {
171
+ out = { ...settled, type: 'directive' };
172
+ }
173
+ if (settled['type'] === 'footnote_ref' && typeof settled['id'] === 'string') {
174
+ const { id, ...rest } = settled;
175
+ out = { ...rest, label: id };
176
+ }
155
177
  return (out ?? node);
156
178
  }
157
179
  /**
package/dist/convert.d.ts CHANGED
@@ -19,6 +19,10 @@ export interface ConvertResult {
19
19
  report: MigrationReport;
20
20
  }
21
21
  export interface ConvertOptions {
22
+ /** Interpret U+E000 as the old generated-space marker in external AST input.
23
+ * Use only for known legacy producers; literal U+E000 cannot be recovered.
24
+ */
25
+ legacySpaceSentinels?: boolean;
22
26
  /**
23
27
  * Stamp attr-wrapper Divs with a `carve-block` key-value marker so the
24
28
  * reverse direction can restore the attrs onto the inner block. Off by