@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 +49 -31
- package/dist/cli.js +32 -9
- package/dist/convert.d.ts +2 -0
- package/dist/convert.js +385 -74
- package/dist/diagnostics.d.ts +17 -0
- package/dist/diagnostics.js +78 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -3
- package/dist/pandoc.d.ts +1 -1
- package/dist/pandoc.js +4 -5
- package/dist/provenance.d.ts +9 -0
- package/dist/provenance.js +31 -0
- package/dist/reverse.d.ts +2 -0
- package/dist/reverse.js +364 -74
- package/package.json +4 -3
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
|
|
113
|
-
|
|
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
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
the
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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.
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
/**
|