@markup-carve/pandoc-carve 0.1.3 → 0.1.5
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 +71 -21
- package/dist/ast-json.js +15 -0
- package/dist/attribute-sanitize.d.ts +6 -0
- package/dist/attribute-sanitize.js +55 -0
- package/dist/cli.js +125 -12
- package/dist/convert.d.ts +2 -1
- package/dist/convert.js +106 -40
- package/dist/diagnostics.d.ts +13 -1
- package/dist/diagnostics.js +64 -41
- package/dist/includes.d.ts +14 -0
- package/dist/includes.js +35 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +52 -2
- package/dist/reverse.js +49 -2
- package/dist/url-scheme.d.ts +26 -0
- package/dist/url-scheme.js +83 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -46,6 +46,12 @@ pandoc-carve doc.crv -t latex -s -o doc.tex
|
|
|
46
46
|
# Just the Pandoc JSON AST (no pandoc needed)
|
|
47
47
|
pandoc-carve doc.crv -t json
|
|
48
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
|
+
|
|
49
55
|
# Read from stdin, pass extra args through to pandoc after --
|
|
50
56
|
cat doc.crv | pandoc-carve - -t latex -- --toc
|
|
51
57
|
|
|
@@ -62,25 +68,51 @@ carve doc.crv --to-json | pandoc-carve - -f carve-json -t latex
|
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
Anything Carve cannot map faithfully is reported on stderr as a
|
|
65
|
-
`pandoc-carve: degraded ...` warning - nothing degrades silently.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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.
|
|
70
81
|
|
|
71
82
|
## API
|
|
72
83
|
|
|
73
84
|
```js
|
|
74
85
|
import { carveToPandoc, carveToPandocJson } from '@markup-carve/pandoc-carve';
|
|
75
86
|
|
|
76
|
-
const { doc, warnings, diagnostics } = carveToPandoc('Hello /world/!');
|
|
87
|
+
const { doc, warnings, diagnostics, report } = carveToPandoc('Hello /world/!');
|
|
77
88
|
// doc = { 'pandoc-api-version': [1, 23, 1], meta: {...}, blocks: [...] }
|
|
78
89
|
// warnings = ['degraded: ...'] for lossy constructs
|
|
79
|
-
// diagnostics = [{ code, direction, severity,
|
|
90
|
+
// diagnostics = [{ code, direction, class, severity, fidelity, confidence, message, ... }]
|
|
91
|
+
// report = { schemaVersion: 2, sourceFormat: 'carve', diagnostics }
|
|
80
92
|
|
|
81
93
|
const json = carveToPandocJson('Hello /world/!'); // stringified doc
|
|
82
94
|
```
|
|
83
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
|
+
```
|
|
111
|
+
|
|
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
|
+
|
|
84
116
|
What the converter reads is the **serialized AST of the Carve spec's PART 12**
|
|
85
117
|
(the shape `resources/ast-schema.json` pins), not any implementation's runtime
|
|
86
118
|
tree - so a document another engine already parsed converts the same way,
|
|
@@ -103,7 +135,7 @@ serializer), so output formatting carries fmt's guarantees:
|
|
|
103
135
|
```js
|
|
104
136
|
import { pandocToCarve, pandocToCarveAst } from '@markup-carve/pandoc-carve';
|
|
105
137
|
|
|
106
|
-
const { carve, warnings, diagnostics } = pandocToCarve(pandocJsonString);
|
|
138
|
+
const { carve, warnings, diagnostics, report } = pandocToCarve(pandocJsonString);
|
|
107
139
|
|
|
108
140
|
// Preserve structured fields that Carve 0.1 source cannot spell, including
|
|
109
141
|
// Pandoc's optional short figure/table caption.
|
|
@@ -160,6 +192,36 @@ The complete node-by-node contract lives in the test goldens. Worked
|
|
|
160
192
|
input/output pairs in both directions - including how interactive constructs
|
|
161
193
|
degrade for print formats - are in [`examples/`](examples/README.md).
|
|
162
194
|
|
|
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
|
+
|
|
163
225
|
## Why a bridge, not a pandoc reader?
|
|
164
226
|
|
|
165
227
|
A native `Text.Pandoc.Readers.Carve` upstream would be a fourth full Carve
|
|
@@ -327,16 +389,4 @@ a processor with neither extension enabled would render.
|
|
|
327
389
|
|
|
328
390
|
## Development
|
|
329
391
|
|
|
330
|
-
|
|
331
|
-
submodule:
|
|
332
|
-
|
|
333
|
-
```sh
|
|
334
|
-
git clone --recurse-submodules https://github.com/markup-carve/pandoc-carve
|
|
335
|
-
# or, in an existing clone:
|
|
336
|
-
git submodule update --init
|
|
337
|
-
npm ci && npm test
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
Without the submodule the corpus tests FAIL rather than skip - a skipped corpus
|
|
341
|
-
reads like a converted one, which is how a stack overflow reachable from a
|
|
342
|
-
two-word document stayed live on the published package.
|
|
392
|
+
Contributor setup, testing, and maintenance notes are in the [development guide](docs/development.md).
|
package/dist/ast-json.js
CHANGED
|
@@ -47,6 +47,8 @@ const CHILD_FIELDS = [
|
|
|
47
47
|
'shortCaption',
|
|
48
48
|
'title',
|
|
49
49
|
'target',
|
|
50
|
+
'old',
|
|
51
|
+
'new',
|
|
50
52
|
];
|
|
51
53
|
function isObject(value) {
|
|
52
54
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -107,6 +109,19 @@ function normalize(node) {
|
|
|
107
109
|
// reads the wire has no reason to know the other spelling existed.
|
|
108
110
|
set('type', 'critic_comment');
|
|
109
111
|
break;
|
|
112
|
+
case 'substitution': {
|
|
113
|
+
if (!Array.isArray(current['old']) && typeof current['oldText'] === 'string') {
|
|
114
|
+
set('old', [{ type: 'text', value: current['oldText'] }]);
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(current['new']) && typeof current['newText'] === 'string') {
|
|
117
|
+
set('new', [{ type: 'text', value: current['newText'] }]);
|
|
118
|
+
}
|
|
119
|
+
if (current['oldText'] !== undefined || current['newText'] !== undefined) {
|
|
120
|
+
const { oldText: _oldText, newText: _newText, ...rest } = out ?? { ...node };
|
|
121
|
+
out = rest;
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
110
125
|
case 'footnote': {
|
|
111
126
|
// The pre-split inline node (carve#405), whose name COLLIDES with
|
|
112
127
|
// the wire `footnote` - which is the DEFINITION block, carrying
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Attribute names Carve never emits, regardless of value. */
|
|
2
|
+
export declare const DANGEROUS_ATTR_NAMES: readonly string[];
|
|
3
|
+
/** Mirror of Carve's always-on PART 9 §25 attribute-name policy. */
|
|
4
|
+
export declare function isDangerousAttrName(name: string): boolean;
|
|
5
|
+
/** Mirror of Carve's always-on PART 9 §25 attribute-value policy. */
|
|
6
|
+
export declare function renderedAttrValue(name: string, value: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DANGEROUS_URL_SCHEMES, SCHEME_PROBE_STRIP_RE } from './url-scheme.js';
|
|
2
|
+
/** Attribute names Carve never emits, regardless of value. */
|
|
3
|
+
export const DANGEROUS_ATTR_NAMES = ['srcdoc', 'formaction'];
|
|
4
|
+
const DANGEROUS_NAMES = new Set(DANGEROUS_ATTR_NAMES);
|
|
5
|
+
const DANGEROUS_VALUE_SCHEMES = new Set(DANGEROUS_URL_SCHEMES);
|
|
6
|
+
const ASCII_WHITESPACE = '\\t\\n\\f\\r ';
|
|
7
|
+
const URL_LIST_SEPARATORS = new Map([
|
|
8
|
+
['srcset', new RegExp(`[,${ASCII_WHITESPACE}]+`)],
|
|
9
|
+
['imagesrcset', new RegExp(`[,${ASCII_WHITESPACE}]+`)],
|
|
10
|
+
['ping', new RegExp(`[${ASCII_WHITESPACE}]+`)],
|
|
11
|
+
['attributionsrc', new RegExp(`[${ASCII_WHITESPACE}]+`)],
|
|
12
|
+
]);
|
|
13
|
+
/** Mirror of Carve's always-on PART 9 §25 attribute-name policy. */
|
|
14
|
+
export function isDangerousAttrName(name) {
|
|
15
|
+
const normalized = name.toLowerCase();
|
|
16
|
+
return normalized.startsWith('on') || DANGEROUS_NAMES.has(normalized);
|
|
17
|
+
}
|
|
18
|
+
function hasDeniedValueScheme(value) {
|
|
19
|
+
const colon = value.indexOf(':');
|
|
20
|
+
if (colon === -1)
|
|
21
|
+
return false;
|
|
22
|
+
const scheme = value.slice(0, colon).replace(SCHEME_PROBE_STRIP_RE, '').toLowerCase();
|
|
23
|
+
return DANGEROUS_VALUE_SCHEMES.has(scheme);
|
|
24
|
+
}
|
|
25
|
+
function decodeCssEscapes(value) {
|
|
26
|
+
return value.replace(/\\([0-9a-f]{1,6}\s?|[\s\S])/gi, (_match, escape) => {
|
|
27
|
+
if (!/^[0-9a-f]/i.test(escape))
|
|
28
|
+
return escape;
|
|
29
|
+
const codePoint = Number.parseInt(escape.trim(), 16);
|
|
30
|
+
return Number.isFinite(codePoint) && codePoint <= 0x10ffff
|
|
31
|
+
? String.fromCodePoint(codePoint)
|
|
32
|
+
: '';
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function hasDangerousCss(value) {
|
|
36
|
+
const compact = decodeCssEscapes(value.replace(/\/\*[\s\S]*?\*\//g, ''))
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/\s+/g, '');
|
|
39
|
+
return compact.includes('expression(')
|
|
40
|
+
|| compact.includes('url(')
|
|
41
|
+
|| compact.includes('@import')
|
|
42
|
+
|| compact.includes('behavior:')
|
|
43
|
+
|| compact.includes('-moz-binding');
|
|
44
|
+
}
|
|
45
|
+
/** Mirror of Carve's always-on PART 9 §25 attribute-value policy. */
|
|
46
|
+
export function renderedAttrValue(name, value) {
|
|
47
|
+
if (hasDeniedValueScheme(value))
|
|
48
|
+
return '';
|
|
49
|
+
const separator = URL_LIST_SEPARATORS.get(name.toLowerCase());
|
|
50
|
+
if (separator && value.split(separator).some((part) => part !== '' && hasDeniedValueScheme(part)))
|
|
51
|
+
return '';
|
|
52
|
+
if (name.toLowerCase() === 'style' && hasDangerousCss(value))
|
|
53
|
+
return '';
|
|
54
|
+
return value;
|
|
55
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Everything after `--` is passed through to pandoc verbatim.
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
12
13
|
import { carveAstToPandoc, carveToPandoc } from './index.js';
|
|
13
|
-
import {
|
|
14
|
+
import { carveToPandocWithIncludes } from './includes.js';
|
|
15
|
+
import { hasLoss, migrationReport } from './diagnostics.js';
|
|
14
16
|
function usage(exitCode) {
|
|
15
17
|
const text = `Usage: pandoc-carve <input | -> [options] [-- pandoc-args...]
|
|
16
18
|
|
|
@@ -36,6 +38,8 @@ Common options:
|
|
|
36
38
|
human warnings are suppressed)
|
|
37
39
|
--fail-on-loss exit 3 when lossy or unsupported diagnostics are present
|
|
38
40
|
--symbols FILE JSON map resolving :name: symbols to text (export)
|
|
41
|
+
--include-root DIR containment root for includes (defaults to input folder)
|
|
42
|
+
--no-includes leave include directives literal
|
|
39
43
|
--pandoc PATH pandoc executable (default: $PANDOC or "pandoc")
|
|
40
44
|
-h, --help show this help
|
|
41
45
|
`;
|
|
@@ -50,6 +54,7 @@ function parseArgs(argv) {
|
|
|
50
54
|
roundtrip: false,
|
|
51
55
|
listTable: true,
|
|
52
56
|
citations: true,
|
|
57
|
+
includes: true,
|
|
53
58
|
failOnLoss: false,
|
|
54
59
|
pandocPath: process.env.PANDOC ?? 'pandoc',
|
|
55
60
|
passthrough: [],
|
|
@@ -88,6 +93,12 @@ function parseArgs(argv) {
|
|
|
88
93
|
else if (a === '--symbols') {
|
|
89
94
|
args.symbolsFile = argv[++i] ?? usage(1);
|
|
90
95
|
}
|
|
96
|
+
else if (a === '--include-root') {
|
|
97
|
+
args.includeRoot = argv[++i] ?? usage(1);
|
|
98
|
+
}
|
|
99
|
+
else if (a === '--no-includes') {
|
|
100
|
+
args.includes = false;
|
|
101
|
+
}
|
|
91
102
|
else if (a === '--diagnostics') {
|
|
92
103
|
args.diagnosticsFile = argv[++i] ?? usage(1);
|
|
93
104
|
}
|
|
@@ -112,6 +123,9 @@ function parseArgs(argv) {
|
|
|
112
123
|
}
|
|
113
124
|
async function main() {
|
|
114
125
|
const args = parseArgs(process.argv.slice(2));
|
|
126
|
+
if (args.from && args.from !== 'carve' && (args.includeRoot || !args.includes)) {
|
|
127
|
+
throw new Error('--include-root and --no-includes only apply to Carve source input');
|
|
128
|
+
}
|
|
115
129
|
// `carve` and `carve-json` are the two EXPORT inputs - Carve source, and the
|
|
116
130
|
// serialized AST any engine writes with --to-json. Every other `-f` names a
|
|
117
131
|
// pandoc reader, which is the import direction.
|
|
@@ -128,17 +142,38 @@ async function main() {
|
|
|
128
142
|
citations: args.citations,
|
|
129
143
|
symbols,
|
|
130
144
|
};
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
if (args.includeRoot && !path.isAbsolute(args.includeRoot)) {
|
|
146
|
+
throw new Error('--include-root must be an absolute path');
|
|
147
|
+
}
|
|
148
|
+
let includeResult;
|
|
149
|
+
let converted;
|
|
150
|
+
if (args.from === 'carve-json') {
|
|
151
|
+
converted = carveAstToPandoc(source, options);
|
|
152
|
+
}
|
|
153
|
+
else if (args.includes && (args.input !== '-' || args.includeRoot)) {
|
|
154
|
+
includeResult = carveToPandocWithIncludes(source, {
|
|
155
|
+
includeRoot: path.resolve(args.includeRoot ?? path.dirname(args.input)),
|
|
156
|
+
...(args.input === '-' ? {} : { sourcePath: path.resolve(args.input) }),
|
|
157
|
+
}, options);
|
|
158
|
+
converted = includeResult;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
converted = carveToPandoc(source, options);
|
|
162
|
+
}
|
|
163
|
+
const includeWarnings = includeResult?.includeWarnings ?? [];
|
|
164
|
+
const suppressedIncludeWarnings = includeResult?.suppressedIncludeWarnings ?? 0;
|
|
165
|
+
const { doc, warnings, diagnostics } = converted;
|
|
166
|
+
const root = args.includeRoot ?? (args.input === '-' ? undefined : path.dirname(path.resolve(args.input)));
|
|
167
|
+
const includeDiagnostics = diagnosticsForIncludes(includeWarnings, suppressedIncludeWarnings, root);
|
|
168
|
+
const allDiagnostics = [...diagnostics, ...includeDiagnostics];
|
|
169
|
+
report(args, warnings, allDiagnostics, 'carve', includeWarnings, suppressedIncludeWarnings, root);
|
|
135
170
|
const json = JSON.stringify(doc);
|
|
136
171
|
if (args.to === 'json') {
|
|
137
172
|
if (args.output)
|
|
138
173
|
writeFileSync(args.output, json + '\n');
|
|
139
174
|
else
|
|
140
175
|
process.stdout.write(json + '\n');
|
|
141
|
-
if (args.failOnLoss && hasLoss(
|
|
176
|
+
if (args.failOnLoss && hasLoss(allDiagnostics))
|
|
142
177
|
process.exitCode = 3;
|
|
143
178
|
return;
|
|
144
179
|
}
|
|
@@ -172,7 +207,7 @@ async function main() {
|
|
|
172
207
|
}
|
|
173
208
|
throw result.error;
|
|
174
209
|
}
|
|
175
|
-
process.exit(result.status || (args.failOnLoss && hasLoss(
|
|
210
|
+
process.exit(result.status || (args.failOnLoss && hasLoss(allDiagnostics) ? 3 : 0));
|
|
176
211
|
}
|
|
177
212
|
/** Reverse direction: pandoc-readable input -> Carve source. */
|
|
178
213
|
async function importToCarve(args) {
|
|
@@ -201,17 +236,29 @@ async function importToCarve(args) {
|
|
|
201
236
|
}
|
|
202
237
|
const { pandocToCarve } = await import('./index.js');
|
|
203
238
|
const { carve, warnings, diagnostics } = pandocToCarve(json);
|
|
204
|
-
|
|
239
|
+
const reportDiagnostics = args.from === 'json' ? diagnostics : [
|
|
240
|
+
...diagnostics,
|
|
241
|
+
{
|
|
242
|
+
code: 'fidelity-unverified',
|
|
243
|
+
direction: 'pandoc-to-carve',
|
|
244
|
+
class: 'unsupported',
|
|
245
|
+
severity: 'error',
|
|
246
|
+
fidelity: 'dropped',
|
|
247
|
+
confidence: 'fallback',
|
|
248
|
+
message: 'Fidelity before the Pandoc JSON boundary was not reported; dropped is a conservative worst-case release-gate classification',
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
report(args, warnings, reportDiagnostics, 'pandoc-json');
|
|
205
252
|
if (args.output)
|
|
206
253
|
writeFileSync(args.output, carve);
|
|
207
254
|
else
|
|
208
255
|
process.stdout.write(carve);
|
|
209
|
-
if (args.failOnLoss && hasLoss(
|
|
256
|
+
if (args.failOnLoss && hasLoss(reportDiagnostics))
|
|
210
257
|
process.exitCode = 3;
|
|
211
258
|
}
|
|
212
|
-
function report(args, warnings, diagnostics) {
|
|
259
|
+
function report(args, warnings, diagnostics, sourceFormat, includeWarnings = [], suppressedIncludeWarnings = 0, includeRoot) {
|
|
213
260
|
if (args.diagnosticsFile !== undefined) {
|
|
214
|
-
const json = JSON.stringify(diagnostics, null, 2) + '\n';
|
|
261
|
+
const json = JSON.stringify(migrationReport(diagnostics, sourceFormat), null, 2) + '\n';
|
|
215
262
|
if (args.diagnosticsFile === '-')
|
|
216
263
|
process.stderr.write(json);
|
|
217
264
|
else
|
|
@@ -220,6 +267,72 @@ function report(args, warnings, diagnostics) {
|
|
|
220
267
|
}
|
|
221
268
|
for (const warning of warnings)
|
|
222
269
|
process.stderr.write(`pandoc-carve: degraded: ${warning}\n`);
|
|
270
|
+
for (const warning of includeWarnings) {
|
|
271
|
+
const file = displayIncludeFile(warning.file, includeRoot, args.input);
|
|
272
|
+
process.stderr.write(`pandoc-carve: ${file}:${warning.line}:${warning.column} ${warning.rule} - ${warning.message}\n`);
|
|
273
|
+
}
|
|
274
|
+
if (suppressedIncludeWarnings) {
|
|
275
|
+
process.stderr.write(`pandoc-carve: ${suppressedIncludeWarnings} additional include warning(s) suppressed\n`);
|
|
276
|
+
}
|
|
277
|
+
for (const diagnostic of diagnostics) {
|
|
278
|
+
if (diagnostic.code === 'fidelity-unverified') {
|
|
279
|
+
process.stderr.write(`pandoc-carve: dropped: ${diagnostic.message}\n`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function diagnosticsForIncludes(warnings, suppressed, root) {
|
|
284
|
+
const diagnostics = warnings.map((warning) => {
|
|
285
|
+
const normalized = NORMALIZED_INCLUDE_RULES.has(warning.rule);
|
|
286
|
+
const file = warning.file ? displayIncludeFile(warning.file, root, '') : undefined;
|
|
287
|
+
return {
|
|
288
|
+
code: warning.rule,
|
|
289
|
+
direction: 'carve-to-pandoc',
|
|
290
|
+
class: normalized ? 'normalized' : 'lossy',
|
|
291
|
+
severity: normalized ? 'info' : 'error',
|
|
292
|
+
fidelity: normalized ? 'normalized' : 'dropped',
|
|
293
|
+
confidence: 'exact',
|
|
294
|
+
message: warning.message,
|
|
295
|
+
details: {
|
|
296
|
+
...(file ? { file } : {}),
|
|
297
|
+
line: warning.line,
|
|
298
|
+
column: warning.column,
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
if (suppressed)
|
|
303
|
+
diagnostics.push({
|
|
304
|
+
code: 'include-warnings-suppressed',
|
|
305
|
+
direction: 'carve-to-pandoc',
|
|
306
|
+
class: 'degraded',
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
fidelity: 'degraded',
|
|
309
|
+
confidence: 'exact',
|
|
310
|
+
message: `${suppressed} additional include warning(s) suppressed`,
|
|
311
|
+
details: { count: suppressed },
|
|
312
|
+
});
|
|
313
|
+
return diagnostics;
|
|
314
|
+
}
|
|
315
|
+
const NORMALIZED_INCLUDE_RULES = new Set([
|
|
316
|
+
'include-footnote-rename',
|
|
317
|
+
'include-heading-clamp',
|
|
318
|
+
'include-heading-id-rename',
|
|
319
|
+
]);
|
|
320
|
+
function displayIncludeFile(file, root, fallback) {
|
|
321
|
+
if (!file || !root)
|
|
322
|
+
return fallback;
|
|
323
|
+
const canonicalRoot = realpathSync(root);
|
|
324
|
+
let canonicalFile = file;
|
|
325
|
+
try {
|
|
326
|
+
canonicalFile = realpathSync(file);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// A resolver may report an unresolved identity. Keep it contained below.
|
|
330
|
+
}
|
|
331
|
+
const relative = path.relative(canonicalRoot, canonicalFile);
|
|
332
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
333
|
+
return '[outside-include-root]';
|
|
334
|
+
}
|
|
335
|
+
return `[include-root]/${relative || '.'}`;
|
|
223
336
|
}
|
|
224
337
|
main().catch((err) => {
|
|
225
338
|
process.stderr.write(`pandoc-carve: ${err instanceof Error ? err.message : String(err)}\n`);
|
package/dist/convert.d.ts
CHANGED
|
@@ -11,11 +11,12 @@
|
|
|
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
|
+
import { type ConversionDiagnostic, type MigrationReport } from './diagnostics.js';
|
|
15
15
|
export interface ConvertResult {
|
|
16
16
|
doc: P.PandocDoc;
|
|
17
17
|
warnings: string[];
|
|
18
18
|
diagnostics: ConversionDiagnostic[];
|
|
19
|
+
report: MigrationReport;
|
|
19
20
|
}
|
|
20
21
|
export interface ConvertOptions {
|
|
21
22
|
/**
|