@markup-carve/pandoc-carve 0.1.0

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/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pandoc-carve CLI.
4
+ *
5
+ * pandoc-carve doc.crv -t latex -o doc.tex
6
+ * pandoc-carve doc.crv -t json # emit Pandoc JSON, no pandoc needed
7
+ * cat doc.crv | pandoc-carve - -t typst -- --toc
8
+ *
9
+ * Everything after `--` is passed through to pandoc verbatim.
10
+ */
11
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pandoc-carve CLI.
4
+ *
5
+ * pandoc-carve doc.crv -t latex -o doc.tex
6
+ * pandoc-carve doc.crv -t json # emit Pandoc JSON, no pandoc needed
7
+ * cat doc.crv | pandoc-carve - -t typst -- --toc
8
+ *
9
+ * Everything after `--` is passed through to pandoc verbatim.
10
+ */
11
+ import { readFileSync, writeFileSync } from 'node:fs';
12
+ import { carveAstToPandoc, carveToPandoc } from './index.js';
13
+ function usage(exitCode) {
14
+ const text = `Usage: pandoc-carve <input | -> [options] [-- pandoc-args...]
15
+
16
+ Export (Carve -> anything pandoc writes):
17
+ -t, --to FORMAT output format (default: json; any pandoc writer, or pdf)
18
+ -f carve-json input is a SERIALIZED Carve AST (spec PART 12) rather than
19
+ Carve source, as written by any engine's "carve --to-json"
20
+
21
+ Import (anything pandoc reads -> Carve):
22
+ -f, --from FORMAT input format (any pandoc reader, or json); output is Carve
23
+
24
+ Common options:
25
+ -o, --output FILE output file (default: stdout; required for -t pdf)
26
+ -s, --standalone produce a standalone document (pandoc -s; export only)
27
+ --roundtrip stamp export with markers so a later import restores
28
+ attribute placement exactly (visible in writer output)
29
+ --no-list-table keep ::: list-table blocks as the degraded div a processor
30
+ without the extension renders (export; default converts
31
+ them to real tables)
32
+ --no-citations read [@key] as an @mention rather than a citation
33
+ (export from Carve source; default reads citations)
34
+ --symbols FILE JSON map resolving :name: symbols to text (export)
35
+ --pandoc PATH pandoc executable (default: $PANDOC or "pandoc")
36
+ -h, --help show this help
37
+ `;
38
+ (exitCode === 0 ? process.stdout : process.stderr).write(text);
39
+ process.exit(exitCode);
40
+ }
41
+ function parseArgs(argv) {
42
+ const args = {
43
+ input: '',
44
+ to: 'json',
45
+ standalone: false,
46
+ roundtrip: false,
47
+ listTable: true,
48
+ citations: true,
49
+ pandocPath: process.env.PANDOC ?? 'pandoc',
50
+ passthrough: [],
51
+ };
52
+ let i = 0;
53
+ while (i < argv.length) {
54
+ const a = argv[i];
55
+ if (a === '--') {
56
+ args.passthrough = argv.slice(i + 1);
57
+ break;
58
+ }
59
+ else if (a === '-h' || a === '--help') {
60
+ usage(0);
61
+ }
62
+ else if (a === '-t' || a === '--to') {
63
+ args.to = argv[++i] ?? usage(1);
64
+ }
65
+ else if (a === '-f' || a === '--from') {
66
+ args.from = argv[++i] ?? usage(1);
67
+ }
68
+ else if (a === '-o' || a === '--output') {
69
+ args.output = argv[++i] ?? usage(1);
70
+ }
71
+ else if (a === '-s' || a === '--standalone') {
72
+ args.standalone = true;
73
+ }
74
+ else if (a === '--roundtrip') {
75
+ args.roundtrip = true;
76
+ }
77
+ else if (a === '--no-list-table') {
78
+ args.listTable = false;
79
+ }
80
+ else if (a === '--no-citations') {
81
+ args.citations = false;
82
+ }
83
+ else if (a === '--symbols') {
84
+ args.symbolsFile = argv[++i] ?? usage(1);
85
+ }
86
+ else if (a === '--pandoc') {
87
+ args.pandocPath = argv[++i] ?? usage(1);
88
+ }
89
+ else if (!args.input) {
90
+ args.input = a;
91
+ }
92
+ else {
93
+ process.stderr.write(`pandoc-carve: unexpected argument: ${a}\n`);
94
+ usage(1);
95
+ }
96
+ i++;
97
+ }
98
+ if (!args.input)
99
+ usage(1);
100
+ return args;
101
+ }
102
+ async function main() {
103
+ const args = parseArgs(process.argv.slice(2));
104
+ // `carve` and `carve-json` are the two EXPORT inputs - Carve source, and the
105
+ // serialized AST any engine writes with --to-json. Every other `-f` names a
106
+ // pandoc reader, which is the import direction.
107
+ if (args.from && args.from !== 'carve' && args.from !== 'carve-json') {
108
+ return importToCarve(args);
109
+ }
110
+ const source = args.input === '-' ? readFileSync(0, 'utf8') : readFileSync(args.input, 'utf8');
111
+ const symbols = args.symbolsFile
112
+ ? JSON.parse(readFileSync(args.symbolsFile, 'utf8'))
113
+ : undefined;
114
+ const options = {
115
+ roundtrip: args.roundtrip,
116
+ listTable: args.listTable,
117
+ citations: args.citations,
118
+ symbols,
119
+ };
120
+ const { doc, warnings } = args.from === 'carve-json'
121
+ ? carveAstToPandoc(source, options)
122
+ : carveToPandoc(source, options);
123
+ for (const w of warnings) {
124
+ process.stderr.write(`pandoc-carve: degraded: ${w}\n`);
125
+ }
126
+ const json = JSON.stringify(doc);
127
+ if (args.to === 'json') {
128
+ if (args.output)
129
+ writeFileSync(args.output, json + '\n');
130
+ else
131
+ process.stdout.write(json + '\n');
132
+ return;
133
+ }
134
+ const { spawnSync } = await import('node:child_process');
135
+ const pandocArgs = ['-f', 'json'];
136
+ if (args.to === 'pdf') {
137
+ if (!args.output) {
138
+ process.stderr.write('pandoc-carve: -t pdf requires -o <file.pdf>\n');
139
+ process.exit(1);
140
+ }
141
+ // pandoc has no "pdf" writer name; the .pdf output path selects it.
142
+ }
143
+ else {
144
+ pandocArgs.push('-t', args.to);
145
+ }
146
+ if (args.standalone)
147
+ pandocArgs.push('-s');
148
+ if (args.output)
149
+ pandocArgs.push('-o', args.output);
150
+ pandocArgs.push(...args.passthrough);
151
+ const result = spawnSync(args.pandocPath, pandocArgs, {
152
+ input: json,
153
+ stdio: ['pipe', 'inherit', 'inherit'],
154
+ maxBuffer: 256 * 1024 * 1024,
155
+ });
156
+ if (result.error) {
157
+ const code = result.error.code;
158
+ if (code === 'ENOENT') {
159
+ process.stderr.write(`pandoc-carve: pandoc executable not found ("${args.pandocPath}"). Install pandoc or use --pandoc PATH / $PANDOC. (-t json needs no pandoc.)\n`);
160
+ process.exit(2);
161
+ }
162
+ throw result.error;
163
+ }
164
+ process.exit(result.status ?? 0);
165
+ }
166
+ /** Reverse direction: pandoc-readable input -> Carve source. */
167
+ async function importToCarve(args) {
168
+ let json;
169
+ if (args.from === 'json') {
170
+ json = args.input === '-' ? readFileSync(0, 'utf8') : readFileSync(args.input, 'utf8');
171
+ }
172
+ else {
173
+ const { spawnSync } = await import('node:child_process');
174
+ const pandocArgs = ['-f', args.from, '-t', 'json', ...args.passthrough];
175
+ const input = args.input === '-' ? readFileSync(0) : readFileSync(args.input);
176
+ const result = spawnSync(args.pandocPath, pandocArgs, {
177
+ input,
178
+ encoding: 'utf8',
179
+ maxBuffer: 256 * 1024 * 1024,
180
+ });
181
+ if (result.error && result.error.code === 'ENOENT') {
182
+ process.stderr.write(`pandoc-carve: pandoc executable not found ("${args.pandocPath}"). Install pandoc or use --pandoc PATH / $PANDOC. (-f json needs no pandoc.)\n`);
183
+ process.exit(2);
184
+ }
185
+ if (result.status !== 0) {
186
+ process.stderr.write(result.stderr ?? '');
187
+ process.exit(result.status ?? 1);
188
+ }
189
+ json = result.stdout;
190
+ }
191
+ 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
+ }
196
+ if (args.output)
197
+ writeFileSync(args.output, carve);
198
+ else
199
+ process.stdout.write(carve);
200
+ }
201
+ main().catch((err) => {
202
+ process.stderr.write(`pandoc-carve: ${err instanceof Error ? err.message : String(err)}\n`);
203
+ process.exit(1);
204
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Carve AST -> Pandoc JSON AST.
3
+ *
4
+ * Walks the SERIALIZED AST that PART 12 of the Carve spec defines - the shape
5
+ * `spec/resources/ast-schema.json` pins, as written by any engine's
6
+ * `--to-json` - and emits a Pandoc document (api-version 1.23.1). It is not the
7
+ * runtime tree of any particular implementation: `src/ast-json.ts` maps that on
8
+ * the way in, so every node type and field name switched on below is spec
9
+ * surface. Anything that cannot be mapped faithfully degrades to a classed
10
+ * Span/Div and reports a warning - nothing degrades silently.
11
+ */
12
+ import type { CarveAstDocument } from './ast-json.js';
13
+ import * as P from './pandoc.js';
14
+ export interface ConvertResult {
15
+ doc: P.PandocDoc;
16
+ warnings: string[];
17
+ }
18
+ export interface ConvertOptions {
19
+ /**
20
+ * Stamp attr-wrapper Divs with a `carve-block` key-value marker so the
21
+ * reverse direction can restore the attrs onto the inner block. Off by
22
+ * default: the marker would otherwise leak into pandoc writer output
23
+ * (e.g. `carve-block="paragraph"` on an HTML div).
24
+ */
25
+ roundtrip?: boolean;
26
+ /**
27
+ * Resolve `:name:` symbols to replacement text (mirrors the renderer's
28
+ * symbols map). Unresolved symbols degrade to a classed Span + warning.
29
+ */
30
+ symbols?: Record<string, string>;
31
+ /**
32
+ * Convert `::: list-table` blocks to real Pandoc tables (the listTable
33
+ * extension's semantics) instead of the degraded Div-of-lists form.
34
+ * Pandoc cells hold full blocks, so nothing is flattened.
35
+ *
36
+ * ON by default, because the REVERSE direction writes a list-table
37
+ * unprompted: `pandocToCarve` reaches for it whenever a pandoc table has a
38
+ * cell holding blocks or row-head columns, which a pipe table cannot spell.
39
+ * With this off, that table came back as a `Div` of nested lists - the
40
+ * bridge lost, on the way in, a construct it had itself chosen on the way
41
+ * out. Set it false to get the literal `Div` a processor without the
42
+ * extension enabled would render.
43
+ */
44
+ listTable?: boolean;
45
+ /**
46
+ * Parse Carve source to the exchange AST's `children`, for the one place a
47
+ * conversion has to read source rather than a tree: block content in
48
+ * metadata (`abstract: |`), whose value is Carve markup inside a YAML
49
+ * string.
50
+ *
51
+ * Supplied as a hook rather than imported, because this module converts a
52
+ * SERIALIZED AST from any engine and has no engine of its own. Without it
53
+ * a block scalar is reported and skipped, which is what it always did.
54
+ */
55
+ parseBlocks?: (source: string) => unknown[];
56
+ }
57
+ export declare function convert(ast: CarveAstDocument, options?: ConvertOptions): ConvertResult;