@markup-carve/pandoc-carve 0.1.2 → 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
 
@@ -197,13 +207,14 @@ 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.
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.
207
218
  - A pandoc table with ROW-HEAD COLUMNS stays a pipe table. `RowHeadColumns` says
208
219
  the leading N cells of every body row are row headers, and a pipe table says
209
220
  that per cell: `|= Mercury | 4,879.4 |` is a `<th scope="row">`. Marking the
@@ -249,7 +260,9 @@ a processor with neither extension enabled would render.
249
260
  those two fields on the way back - the label table is section 4.2's, it lives
250
261
  in the engine, and a second copy here would drift. The locator TEXT round-
251
262
  trips byte for byte, so re-parsing the emitted source with the citations
252
- 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
253
266
  `NormalCitation` cannot be spelled in Carve (the integral `+` is a property of
254
267
  the whole cluster) and is imported as integral with a warning.
255
268
  - Pandoc keeps its bibliography in document metadata, not in the AST, so
@@ -266,11 +279,18 @@ a processor with neither extension enabled would render.
266
279
  lists, lists of maps, scalars, booleans - round-trips too.
267
280
  - A `rowGroups` partition (a foot, several body groups, a body's own header
268
281
  rows or row-head columns) survives into the exchange AST, so
269
- `pandocToCarveAst` hands it on whole - but PART 9 section 16's pipe table
270
- spells only a leading run of header rows, so `pandocToCarve` flattens the
271
- rest into body rows and says so. Row-head columns are the exception - the pipe
272
- table marks them on the cells (above). Use the AST entry point when a foot or
273
- 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.
274
294
  - A marker on a HEAD cell (`|=> Name |`) is the column's alignment and becomes
275
295
  pandoc's `ColSpec`; a marker on a BODY cell (`|> 12 |`) aligns that cell alone
276
296
  and becomes the cell's own `Alignment`. The two are not interchangeable: a
@@ -320,7 +340,3 @@ npm ci && npm test
320
340
  Without the submodule the corpus tests FAIL rather than skip - a skipped corpus
321
341
  reads like a converted one, which is how a stack overflow reachable from a
322
342
  two-word document stayed live on the published package.
323
-
324
- ## License
325
-
326
- 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
  /**