@markup-carve/pandoc-carve 0.1.0 → 0.1.3

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
@@ -62,16 +62,21 @@ carve doc.crv --to-json | pandoc-carve - -f carve-json -t latex
62
62
  ```
63
63
 
64
64
  Anything Carve cannot map faithfully is reported on stderr as a
65
- `pandoc-carve: degraded ...` warning - nothing degrades silently.
65
+ `pandoc-carve: degraded ...` warning - nothing degrades silently. For migration
66
+ automation, `--diagnostics report.json` writes the same findings as structured
67
+ JSON without mixing them into document output. `--fail-on-loss` exits with code
68
+ 3 for `lossy` or `unsupported` findings, while harmless `normalized` findings
69
+ do not fail CI. Use `--diagnostics -` for JSON on stderr.
66
70
 
67
71
  ## API
68
72
 
69
73
  ```js
70
74
  import { carveToPandoc, carveToPandocJson } from '@markup-carve/pandoc-carve';
71
75
 
72
- const { doc, warnings } = carveToPandoc('Hello /world/!');
76
+ const { doc, warnings, diagnostics } = carveToPandoc('Hello /world/!');
73
77
  // doc = { 'pandoc-api-version': [1, 23, 1], meta: {...}, blocks: [...] }
74
78
  // warnings = ['degraded: ...'] for lossy constructs
79
+ // diagnostics = [{ code, direction, severity, message, details?, path?, sourceLocation? }]
75
80
 
76
81
  const json = carveToPandocJson('Hello /world/!'); // stringified doc
77
82
  ```
@@ -85,7 +90,7 @@ whether it arrives as an object or as JSON text:
85
90
  import { carveAstToPandoc, carveToCarveAst } from '@markup-carve/pandoc-carve';
86
91
 
87
92
  // A tree from carve-rs, carve-php, carve-go ... or `carve doc.crv --to-json`
88
- const { doc, warnings } = carveAstToPandoc(serializedAstJson);
93
+ const { doc, warnings, diagnostics } = carveAstToPandoc(serializedAstJson);
89
94
 
90
95
  // The same exchange format, produced from source here
91
96
  const ast = carveToCarveAst('Hello /world/!');
@@ -98,7 +103,7 @@ serializer), so output formatting carries fmt's guarantees:
98
103
  ```js
99
104
  import { pandocToCarve, pandocToCarveAst } from '@markup-carve/pandoc-carve';
100
105
 
101
- const { carve, warnings } = pandocToCarve(pandocJsonString);
106
+ const { carve, warnings, diagnostics } = pandocToCarve(pandocJsonString);
102
107
 
103
108
  // Preserve structured fields that Carve 0.1 source cannot spell, including
104
109
  // Pandoc's optional short figure/table caption.
@@ -109,8 +114,13 @@ Round-trips are tested as a hard gate: `carve -> pandoc AST -> carve` must
109
114
  render byte-identical HTML to the original source across the test corpus.
110
115
  For exact restoration of attribute placement (`{.lead}` on a paragraph or
111
116
  list), export with `carveToPandoc(src, { roundtrip: true })` or the CLI's
112
- `--roundtrip` flag - it stamps wrapper divs with a `carve-block` marker the
113
- importer uses, at the cost of that marker being visible in writer output.
117
+ `--roundtrip` flag. In addition to the existing `carve-block` attribute marker,
118
+ this preserves comments, typed citation fields, and unknown future Carve nodes
119
+ with a versioned private Span/Div envelope. The native citation and readable
120
+ unknown-node fallback remain usable by Pandoc. The marker may be visible in
121
+ writer output and exact preservation is guaranteed only through Pandoc JSON.
122
+ The complete format, safety checks, available source information, and filter
123
+ semantics are documented in [the provenance envelope](docs/roundtrip-provenance.md).
114
124
 
115
125
  Pipe the JSON to pandoc yourself:
116
126
 
@@ -144,7 +154,7 @@ node -e "import('@markup-carve/pandoc-carve').then(m => process.stdout.write(m.c
144
154
  | `::: \|` line blocks (verse) | LineBlock, one entry per line, an empty entry per stanza break |
145
155
  | 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 |
146
156
  | A no-break space the parser resolved (`\ `) | U+00A0 (the engines publish a private-use sentinel for it) |
147
- | A reference link or image nothing defines (`[r][]`) | the literal source as text, which is what Carve renders, plus a warning naming the label - as an unresolved footnote already did |
157
+ | 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 |
148
158
 
149
159
  The complete node-by-node contract lives in the test goldens. Worked
150
160
  input/output pairs in both directions - including how interactive constructs
@@ -197,20 +207,23 @@ a processor with neither extension enabled would render.
197
207
  against `resources/ast-schema.json` can still be incoherent - the bridge
198
208
  checks it itself rather than trusting a green validator.
199
209
  - A pandoc table with block content in a cell (a list, two paragraphs, a code
200
- block) is imported as `::: list-table` rather than a pipe table. PART 9 §16's
201
- pipe-table cell holds inlines, so there is no pipe form for it, and the
202
- extension's cells are list items that hold full blocks. Structure is
203
- preserved; three things the extension does not spell are reported instead -
204
- per-column alignment, a foot, and a body group's intermediate header rows.
205
- It converts back to the pandoc table by default; `listTable: false` returns
206
- the degraded div instead.
207
- - A pandoc table with ROW-HEAD COLUMNS takes the same route on the source path,
208
- because `header-cols=N` is exactly pandoc's `RowHeadColumns` and a pipe table
209
- cannot say it. Only when every body group agrees on the count: `header-cols`
210
- is one number for the whole table, so a table whose bodies differ keeps the
211
- pipe form rather than come back with row headers ADDED to the rows that had
212
- none. The AST path keeps the plain table, where `rowGroups` carries the whole
213
- partition and nothing has to be traded for it.
210
+ block) is imported as `::: list-table` rather than a pipe table, and that is
211
+ now the ONLY reason a table leaves the pipe form. PART 9 §16's pipe-table cell
212
+ holds inlines, so there is no pipe form for it, and the extension's cells are
213
+ list items that hold full blocks. Structure is preserved; what the extension
214
+ cannot spell is reported instead - a body group's attributes, a body boundary
215
+ no header row marks, and body groups that disagree on their row-head column
216
+ count. It converts back to the pandoc table by default; `listTable: false`
217
+ returns the degraded div instead.
218
+ - A pandoc table with ROW-HEAD COLUMNS stays a pipe table. `RowHeadColumns` says
219
+ the leading N cells of every body row are row headers, and a pipe table says
220
+ that per cell: `|= Mercury | 4,879.4 |` is a `<th scope="row">`. Marking the
221
+ cells needs no extension and round-trips, because the export direction derives
222
+ the count back from the leading run.
223
+ - On the way out, one TableBody per RUN of body rows that agree on that leading
224
+ run. Pandoc's `RowHeadColumns` is a count on a body and a `Table` holds a list
225
+ of bodies, so rows that disagree are simply different bodies. A table whose
226
+ body rows all agree emits exactly one body, as before.
214
227
  - Pandoc `SmallCaps` has no Carve node and is not getting one: it imports as a
215
228
  `[text]{.smallcaps}` span, with a warning saying so. The span is not a dead
216
229
  end, though - the export direction reads that class back as `SmallCaps`, the
@@ -247,7 +260,9 @@ a processor with neither extension enabled would render.
247
260
  those two fields on the way back - the label table is section 4.2's, it lives
248
261
  in the engine, and a second copy here would drift. The locator TEXT round-
249
262
  trips byte for byte, so re-parsing the emitted source with the citations
250
- extension restores the typed pair. A group whose items mix `AuthorInText` and
263
+ extension restores the typed pair. With `roundtrip: true`, the private
264
+ provenance wrapper retains the original typed fields directly while its
265
+ enclosed native `Cite` remains available to citeproc. A group whose items mix `AuthorInText` and
251
266
  `NormalCitation` cannot be spelled in Carve (the integral `+` is a property of
252
267
  the whole cluster) and is imported as integral with a warning.
253
268
  - Pandoc keeps its bibliography in document metadata, not in the AST, so
@@ -264,11 +279,18 @@ a processor with neither extension enabled would render.
264
279
  lists, lists of maps, scalars, booleans - round-trips too.
265
280
  - A `rowGroups` partition (a foot, several body groups, a body's own header
266
281
  rows or row-head columns) survives into the exchange AST, so
267
- `pandocToCarveAst` hands it on whole - but PART 9 section 16's pipe table
268
- spells only a leading run of header rows, so `pandocToCarve` flattens the
269
- rest into body rows and says so. Row-head columns are the exception - they
270
- have a list-table spelling and take it (above). Use the AST entry point when
271
- a foot or a second body group has to survive.
282
+ `pandocToCarveAst` hands it on whole. `pandocToCarve` spells most of it too: a
283
+ pipe table states its head and foot row counts on its attribute line
284
+ (`{header-rows=2 footer-rows=1}`) and marks its row headers on the cells, so
285
+ what flattens into body rows is a second body group and a body's own
286
+ intermediate header rows and attributes - reported, not dropped quietly. Use
287
+ the AST entry point when a second body group has to survive.
288
+ - Stating the foot states the whole partition, and a stated partition is ONE
289
+ body. A body carries one `RowHeadColumns`, so when the body rows disagree on
290
+ how many leading cells are row headers, a table with a foot cannot keep them:
291
+ the reader takes a count every row agrees on, and the disagreement is reported
292
+ from both directions. Without a foot the reader splits a body at every change
293
+ and every run survives.
272
294
  - A marker on a HEAD cell (`|=> Name |`) is the column's alignment and becomes
273
295
  pandoc's `ColSpec`; a marker on a BODY cell (`|> 12 |`) aligns that cell alone
274
296
  and becomes the cell's own `Alignment`. The two are not interchangeable: a
@@ -318,7 +340,3 @@ npm ci && npm test
318
340
  Without the submodule the corpus tests FAIL rather than skip - a skipped corpus
319
341
  reads like a converted one, which is how a stack overflow reachable from a
320
342
  two-word document stayed live on the published package.
321
-
322
- ## License
323
-
324
- MIT
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { readFileSync, writeFileSync } from 'node:fs';
12
12
  import { carveAstToPandoc, carveToPandoc } from './index.js';
13
+ import { hasLoss } from './diagnostics.js';
13
14
  function usage(exitCode) {
14
15
  const text = `Usage: pandoc-carve <input | -> [options] [-- pandoc-args...]
15
16
 
@@ -31,6 +32,9 @@ Common options:
31
32
  them to real tables)
32
33
  --no-citations read [@key] as an @mention rather than a citation
33
34
  (export from Carve source; default reads citations)
35
+ --diagnostics FILE write structured diagnostics as JSON (use - for stderr;
36
+ human warnings are suppressed)
37
+ --fail-on-loss exit 3 when lossy or unsupported diagnostics are present
34
38
  --symbols FILE JSON map resolving :name: symbols to text (export)
35
39
  --pandoc PATH pandoc executable (default: $PANDOC or "pandoc")
36
40
  -h, --help show this help
@@ -46,6 +50,7 @@ function parseArgs(argv) {
46
50
  roundtrip: false,
47
51
  listTable: true,
48
52
  citations: true,
53
+ failOnLoss: false,
49
54
  pandocPath: process.env.PANDOC ?? 'pandoc',
50
55
  passthrough: [],
51
56
  };
@@ -83,6 +88,12 @@ function parseArgs(argv) {
83
88
  else if (a === '--symbols') {
84
89
  args.symbolsFile = argv[++i] ?? usage(1);
85
90
  }
91
+ else if (a === '--diagnostics') {
92
+ args.diagnosticsFile = argv[++i] ?? usage(1);
93
+ }
94
+ else if (a === '--fail-on-loss') {
95
+ args.failOnLoss = true;
96
+ }
86
97
  else if (a === '--pandoc') {
87
98
  args.pandocPath = argv[++i] ?? usage(1);
88
99
  }
@@ -117,18 +128,18 @@ async function main() {
117
128
  citations: args.citations,
118
129
  symbols,
119
130
  };
120
- const { doc, warnings } = args.from === 'carve-json'
131
+ const { doc, warnings, diagnostics } = args.from === 'carve-json'
121
132
  ? carveAstToPandoc(source, options)
122
133
  : carveToPandoc(source, options);
123
- for (const w of warnings) {
124
- process.stderr.write(`pandoc-carve: degraded: ${w}\n`);
125
- }
134
+ report(args, warnings, diagnostics);
126
135
  const json = JSON.stringify(doc);
127
136
  if (args.to === 'json') {
128
137
  if (args.output)
129
138
  writeFileSync(args.output, json + '\n');
130
139
  else
131
140
  process.stdout.write(json + '\n');
141
+ if (args.failOnLoss && hasLoss(diagnostics))
142
+ process.exitCode = 3;
132
143
  return;
133
144
  }
134
145
  const { spawnSync } = await import('node:child_process');
@@ -161,7 +172,7 @@ async function main() {
161
172
  }
162
173
  throw result.error;
163
174
  }
164
- process.exit(result.status ?? 0);
175
+ process.exit(result.status || (args.failOnLoss && hasLoss(diagnostics) ? 3 : 0));
165
176
  }
166
177
  /** Reverse direction: pandoc-readable input -> Carve source. */
167
178
  async function importToCarve(args) {
@@ -189,14 +200,26 @@ async function importToCarve(args) {
189
200
  json = result.stdout;
190
201
  }
191
202
  const { pandocToCarve } = await import('./index.js');
192
- const { carve, warnings } = pandocToCarve(json);
193
- for (const w of warnings) {
194
- process.stderr.write(`pandoc-carve: degraded: ${w}\n`);
195
- }
203
+ const { carve, warnings, diagnostics } = pandocToCarve(json);
204
+ report(args, warnings, diagnostics);
196
205
  if (args.output)
197
206
  writeFileSync(args.output, carve);
198
207
  else
199
208
  process.stdout.write(carve);
209
+ if (args.failOnLoss && hasLoss(diagnostics))
210
+ process.exitCode = 3;
211
+ }
212
+ function report(args, warnings, diagnostics) {
213
+ if (args.diagnosticsFile !== undefined) {
214
+ const json = JSON.stringify(diagnostics, null, 2) + '\n';
215
+ if (args.diagnosticsFile === '-')
216
+ process.stderr.write(json);
217
+ else
218
+ writeFileSync(args.diagnosticsFile, json);
219
+ return;
220
+ }
221
+ for (const warning of warnings)
222
+ process.stderr.write(`pandoc-carve: degraded: ${warning}\n`);
200
223
  }
201
224
  main().catch((err) => {
202
225
  process.stderr.write(`pandoc-carve: ${err instanceof Error ? err.message : String(err)}\n`);
package/dist/convert.d.ts CHANGED
@@ -11,9 +11,11 @@
11
11
  */
12
12
  import type { CarveAstDocument } from './ast-json.js';
13
13
  import * as P from './pandoc.js';
14
+ import { type ConversionDiagnostic } from './diagnostics.js';
14
15
  export interface ConvertResult {
15
16
  doc: P.PandocDoc;
16
17
  warnings: string[];
18
+ diagnostics: ConversionDiagnostic[];
17
19
  }
18
20
  export interface ConvertOptions {
19
21
  /**