@md2nativedocx/cli 0.0.0-stage → 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/README.md +90 -2
- package/assets/README.md +27 -0
- package/assets/reference.docx +0 -0
- package/bin/md2nativedocx.mjs +274 -0
- package/package.json +61 -4
- package/src/chartParts.mjs +177 -0
- package/src/cliSupport.mjs +28 -0
- package/src/convert.d.mts +91 -0
- package/src/convert.mjs +202 -0
- package/src/envOptions.mjs +88 -0
- package/src/postprocess.mjs +628 -0
- package/src/pptxExport.mjs +95 -0
- package/src/referenceDocBuilder.mjs +517 -0
- package/src/zipUtils.mjs +38 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types of `@md2nativedocx/cli`'s library entry point (`convert.mjs`). Hand-written: the package is plain
|
|
3
|
+
* JavaScript with no build step. Keep in step with `convert.mjs` and `envOptions.mjs`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Page and typography of the bundled reference document. Ignored under {@link ConvertOptions.referenceDoc}. */
|
|
7
|
+
export interface LayoutOptions {
|
|
8
|
+
/** `A3`, `A4` (default), `Letter`, `Legal`… An unknown value falls back to A4. */
|
|
9
|
+
pageSize?: string;
|
|
10
|
+
/** `portrait` (default) or `landscape`. */
|
|
11
|
+
orientation?: string;
|
|
12
|
+
/** Word's presets: `normal` (default), `narrow`, `moderate`, `wide`. */
|
|
13
|
+
margins?: string;
|
|
14
|
+
/** Custom margins in centimetres; each side overrides the preset. */
|
|
15
|
+
marginsCustomCm?: { top?: number; right?: number; bottom?: number; left?: number };
|
|
16
|
+
headingFont?: string;
|
|
17
|
+
bodyFont?: string;
|
|
18
|
+
/** Body text size in points. */
|
|
19
|
+
fontSizePt?: number;
|
|
20
|
+
/** `single`, `1.15`, `1.5`, `double`; anything else keeps the template's spacing. */
|
|
21
|
+
lineSpacing?: string;
|
|
22
|
+
/** `both`, `right` or `center`; anything else leaves body text left-aligned. */
|
|
23
|
+
justify?: string;
|
|
24
|
+
/** Theme accent colour, `RRGGBB`. */
|
|
25
|
+
accentColor?: string;
|
|
26
|
+
/** Table header fill, `RRGGBB`. */
|
|
27
|
+
tableHeaderColor?: string;
|
|
28
|
+
/** A centred page number in the footer. */
|
|
29
|
+
footerPageNumber?: boolean;
|
|
30
|
+
/** Put each heading immediately followed by a table in its own landscape section. */
|
|
31
|
+
landscapeTables?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** How {@link convert} builds the document. Every field is optional; the defaults match the CLI's. */
|
|
35
|
+
export interface ConvertOptions {
|
|
36
|
+
/** Pandoc's working directory, against which relative image paths resolve. Default: `process.cwd()`. */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
/** A `.docx` template to use instead of the bundled one (Pandoc's `--reference-doc`). Must exist. */
|
|
39
|
+
referenceDoc?: string;
|
|
40
|
+
layout?: LayoutOptions;
|
|
41
|
+
/** A table of contents after the title, refreshed when Word opens the file. Default `false`. */
|
|
42
|
+
toc?: boolean;
|
|
43
|
+
/** Heading levels in the table of contents, 2 to 4. Default 3. */
|
|
44
|
+
tocDepth?: number;
|
|
45
|
+
/** Force a colour emoji font on emoji runs. Default `true`. */
|
|
46
|
+
emojiFont?: boolean;
|
|
47
|
+
/** SmartArt for the diagrams that can be one. Default `true`. */
|
|
48
|
+
smartArt?: boolean;
|
|
49
|
+
/** SmartArt look; unknown values fall back to `colorful`. */
|
|
50
|
+
smartArtStyle?: string;
|
|
51
|
+
/** Ship the pre-rendered SmartArt drawing, so Word and LibreOffice show the same geometry. Default `true`. */
|
|
52
|
+
smartArtDrawing?: boolean;
|
|
53
|
+
/** Native Word charts for `pie` / `xychart` / `radar`: `true` (default), `false`, or the types to chart. */
|
|
54
|
+
nativeCharts?: boolean | readonly string[];
|
|
55
|
+
/** Largest diagram width in EMU. Default: the page's usable width. */
|
|
56
|
+
maxDrawingCx?: number;
|
|
57
|
+
/** Largest diagram height in EMU. Default: the page's usable height. */
|
|
58
|
+
maxDrawingCy?: number;
|
|
59
|
+
/** Pandoc executable. Default `pandoc`, looked up on `PATH`. */
|
|
60
|
+
pandocBin?: string;
|
|
61
|
+
/** Node executable the Lua filter runs the diagram bridge with. Default: `process.execPath`. */
|
|
62
|
+
nodeBin?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A finished document. */
|
|
66
|
+
export interface ConvertResult {
|
|
67
|
+
/** The `.docx` file's bytes. */
|
|
68
|
+
document: Buffer;
|
|
69
|
+
/** Non-fatal problems in the diagrams (syntax ignored, a SmartArt or chart drawn as shapes instead). */
|
|
70
|
+
warnings: string[];
|
|
71
|
+
/** Everything Pandoc and the filter wrote to stderr, for a log. */
|
|
72
|
+
pandocStderr: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A conversion that produced no document. */
|
|
76
|
+
export declare class ConversionError extends Error {
|
|
77
|
+
/** `setup`: filter or reference document · `pandoc`: Pandoc failed or is missing · `postprocess`. */
|
|
78
|
+
readonly stage: 'setup' | 'pandoc' | 'postprocess';
|
|
79
|
+
/** Pandoc's stderr, when it ran. */
|
|
80
|
+
readonly pandocStderr: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Convert a Markdown document to `.docx`: Pandoc with the md2nativedocx filter, every ```mermaid block as
|
|
85
|
+
* editable Word shapes, SmartArt or a native chart. Needs Pandoc installed. Never reads `MD2NATIVEDOCX_*`
|
|
86
|
+
* environment variables: behavior comes from `options` only.
|
|
87
|
+
*
|
|
88
|
+
* @param source Markdown text, or `{ path }` of a file for Pandoc to read (`.qmd` is read as Markdown).
|
|
89
|
+
* @throws {ConversionError} when no document could be produced.
|
|
90
|
+
*/
|
|
91
|
+
export declare function convert(source: string | { path: string }, options?: ConvertOptions): Promise<ConvertResult>;
|
package/src/convert.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `convert()`: Markdown in, finished `.docx` bytes out (docs/specs/01-v2-engine-spec.md §6, ADR 0012).
|
|
3
|
+
*
|
|
4
|
+
* The library entry point of `@md2nativedocx/cli`, and what `bin/md2nativedocx.mjs` itself calls. It owns
|
|
5
|
+
* the whole document path: reference document build, Pandoc with the Lua filter (which renders each
|
|
6
|
+
* ```mermaid block through `renderDiagram()`), post-processing, SmartArt and chart part injection. It
|
|
7
|
+
* lives here and not in `core` because it runs Pandoc (AGENTS.md: core knows nothing about Pandoc).
|
|
8
|
+
*
|
|
9
|
+
* Behavior comes from {@link ConvertOptions} only. Every inherited `MD2NATIVEDOCX_*` variable is removed
|
|
10
|
+
* from the environment Pandoc and the filter see, then the ones the filter and the bridge read are set from
|
|
11
|
+
* the options, so a host process's environment cannot change a conversion behind its caller's back. The
|
|
12
|
+
* variables remain the CLI's interface (`envOptions.mjs` reads them).
|
|
13
|
+
*
|
|
14
|
+
* Security (AGENTS.md): Pandoc runs through `execFile` with an argument array (rule 4); every file is written
|
|
15
|
+
* in a fresh temporary directory removed afterwards; the post-processing is the rule 7 allowlist.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFile } from 'node:child_process';
|
|
19
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { extname, join } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { postProcessDocx, injectSmartArtParts } from './postprocess.mjs';
|
|
24
|
+
import { injectChartParts } from './chartParts.mjs';
|
|
25
|
+
import { buildReferenceDoc, resolveMaxDrawingExtentEmu, resolvePageSize, resolveMargins } from './referenceDocBuilder.mjs';
|
|
26
|
+
|
|
27
|
+
/** A conversion that produced no document. `stage` says where it stopped. */
|
|
28
|
+
export class ConversionError extends Error {
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} message
|
|
31
|
+
* @param {{ stage: 'setup' | 'pandoc' | 'postprocess', pandocStderr?: string, cause?: unknown }} details
|
|
32
|
+
*/
|
|
33
|
+
constructor(message, { stage, pandocStderr = '', cause } = {}) {
|
|
34
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
35
|
+
this.name = 'ConversionError';
|
|
36
|
+
this.stage = stage;
|
|
37
|
+
this.pandocStderr = pandocStderr;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Custom reference.docx (Word's current default look: Aptos font scheme, modern "Office" theme colors,
|
|
42
|
+
// non-bold flat heading hierarchy) instead of Pandoc's own bundled default, which is still the original
|
|
43
|
+
// 2007-2010 Office theme. See packages/cli/assets/README.md for how it was built and what it changes.
|
|
44
|
+
// `src/` and `bin/` are at the same depth, so this also holds in the VS Code extension's vendored bundle.
|
|
45
|
+
const DEFAULT_REFERENCE_DOC = fileURLToPath(new URL('../assets/reference.docx', import.meta.url));
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where @md2nativedocx/pandoc-filter lands varies with how the CLI was deployed (npm workspace, nested or
|
|
49
|
+
* flat install, the VS Code extension's vendored copy). import.meta.resolve() is Node's own resolution
|
|
50
|
+
* algorithm, so it finds it in every case without guessing directory depths.
|
|
51
|
+
*/
|
|
52
|
+
function resolveFilterPath() {
|
|
53
|
+
try {
|
|
54
|
+
return fileURLToPath(import.meta.resolve('@md2nativedocx/pandoc-filter/md2nativedocx.lua'));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new ConversionError(`could not locate @md2nativedocx/pandoc-filter: ${errorMessage(err)}`, { stage: 'setup', cause: err });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function errorMessage(err) {
|
|
61
|
+
return err instanceof Error ? err.message : String(err);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** `MD2NATIVEDOCX_NATIVE_CHARTS` as the bridge reads it, or `null` for charts off. */
|
|
65
|
+
function nativeChartsEnv(setting) {
|
|
66
|
+
if (setting === undefined || setting === true) return '1';
|
|
67
|
+
if (setting === false || (Array.isArray(setting) && setting.length === 0)) return null;
|
|
68
|
+
return setting.join(',');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The environment Pandoc runs with: the caller's, minus `MD2NATIVEDOCX_*`, plus what `options` says. */
|
|
72
|
+
function pandocEnvironment(options, { smartArtDir, chartDir, maxDrawingExtentEmu, landscapeGeometry }) {
|
|
73
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('MD2NATIVEDOCX_')));
|
|
74
|
+
if (smartArtDir) env.MD2NATIVEDOCX_SMARTART_DIR = smartArtDir;
|
|
75
|
+
if (options.smartArtStyle !== undefined) env.MD2NATIVEDOCX_SMARTART_STYLE = options.smartArtStyle;
|
|
76
|
+
if (options.smartArtDrawing === false) env.MD2NATIVEDOCX_SMARTART_DRAWING = '0';
|
|
77
|
+
if (chartDir) {
|
|
78
|
+
env.MD2NATIVEDOCX_CHART_DIR = chartDir;
|
|
79
|
+
env.MD2NATIVEDOCX_NATIVE_CHARTS = nativeChartsEnv(options.nativeCharts);
|
|
80
|
+
}
|
|
81
|
+
// md2nativedocx.lua needs it on Windows (no shebang handling there, so it cannot run a bare .mjs). Our
|
|
82
|
+
// own interpreter is always Node: a system Node, or the VS Code extension's Electron run as Node.
|
|
83
|
+
env.MD2NATIVEDOCX_NODE_BIN = options.nodeBin ?? process.execPath;
|
|
84
|
+
// An explicit drawing cap wins over the page-derived one (e.g. a diagram meant for a half-width cell).
|
|
85
|
+
const cx = options.maxDrawingCx ?? (maxDrawingExtentEmu ? Math.round(maxDrawingExtentEmu.cx) : undefined);
|
|
86
|
+
const cy = options.maxDrawingCy ?? (maxDrawingExtentEmu ? Math.round(maxDrawingExtentEmu.cy) : undefined);
|
|
87
|
+
if (cx !== undefined) env.MD2NATIVEDOCX_MAX_DRAWING_CX = String(cx);
|
|
88
|
+
if (cy !== undefined) env.MD2NATIVEDOCX_MAX_DRAWING_CY = String(cy);
|
|
89
|
+
if (landscapeGeometry) {
|
|
90
|
+
env.MD2NATIVEDOCX_LANDSCAPE_TABLES = '1';
|
|
91
|
+
env.MD2NATIVEDOCX_PAGE_W_TWIPS = String(landscapeGeometry.pgSize.w);
|
|
92
|
+
env.MD2NATIVEDOCX_PAGE_H_TWIPS = String(landscapeGeometry.pgSize.h);
|
|
93
|
+
env.MD2NATIVEDOCX_MARGIN_TOP_TWIPS = String(landscapeGeometry.margins.top);
|
|
94
|
+
env.MD2NATIVEDOCX_MARGIN_RIGHT_TWIPS = String(landscapeGeometry.margins.right);
|
|
95
|
+
env.MD2NATIVEDOCX_MARGIN_BOTTOM_TWIPS = String(landscapeGeometry.margins.bottom);
|
|
96
|
+
env.MD2NATIVEDOCX_MARGIN_LEFT_TWIPS = String(landscapeGeometry.margins.left);
|
|
97
|
+
}
|
|
98
|
+
return env;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runPandoc(bin, args, { cwd, env }) {
|
|
102
|
+
return new Promise((resolvePromise, reject) => {
|
|
103
|
+
execFile(bin, args, { cwd, env, maxBuffer: 64 * 1024 * 1024 }, (err, _stdout, stderr) => {
|
|
104
|
+
if (err) {
|
|
105
|
+
reject(new ConversionError(`Pandoc failed (exit ${err.code ?? '?'})`, { stage: 'pandoc', pandocStderr: stderr ?? '', cause: err }));
|
|
106
|
+
} else {
|
|
107
|
+
resolvePromise(stderr ?? '');
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Lines this project wrote to stderr (`md2nativedocx: ...`: parser warnings, SmartArt or chart fallbacks,
|
|
115
|
+
* from each bridge run the Lua filter spawned), without that prefix. Pandoc's own diagnostics share the
|
|
116
|
+
* stream but are not ours to count.
|
|
117
|
+
*/
|
|
118
|
+
function extractWarnings(stderrText) {
|
|
119
|
+
return stderrText
|
|
120
|
+
.split('\n')
|
|
121
|
+
.filter((line) => line.startsWith('md2nativedocx: ') && line.trim().length > 0)
|
|
122
|
+
.map((line) => line.slice('md2nativedocx: '.length));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convert a Markdown document to `.docx`. See `convert.d.mts` for the option and result types.
|
|
127
|
+
*
|
|
128
|
+
* @param {string | { path: string }} source Markdown text, or a file for Pandoc to read (its extension picks
|
|
129
|
+
* the reader; `.qmd` is read as Markdown).
|
|
130
|
+
* @param {import('./convert.d.mts').ConvertOptions} [options]
|
|
131
|
+
* @returns {Promise<import('./convert.d.mts').ConvertResult>}
|
|
132
|
+
*/
|
|
133
|
+
export async function convert(source, options = {}) {
|
|
134
|
+
const filterPath = resolveFilterPath();
|
|
135
|
+
const cwd = options.cwd ?? process.cwd();
|
|
136
|
+
const customReferenceDoc = options.referenceDoc;
|
|
137
|
+
if (customReferenceDoc !== undefined && !existsSync(customReferenceDoc)) {
|
|
138
|
+
throw new ConversionError(`reference document not found: ${customReferenceDoc}`, { stage: 'setup' });
|
|
139
|
+
}
|
|
140
|
+
// A custom reference document wins outright: its page setup is unknown, so page/typography options are
|
|
141
|
+
// not patched into it (spec §2.1, option (a)).
|
|
142
|
+
const layout = customReferenceDoc ? {} : (options.layout ?? {});
|
|
143
|
+
|
|
144
|
+
const work = mkdtempSync(join(tmpdir(), 'md2nativedocx-convert-'));
|
|
145
|
+
let generatedReferenceDoc = null;
|
|
146
|
+
try {
|
|
147
|
+
try {
|
|
148
|
+
generatedReferenceDoc = customReferenceDoc ? null : buildReferenceDoc(DEFAULT_REFERENCE_DOC, layout);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw new ConversionError(`reference document setup failed: ${errorMessage(err)}`, { stage: 'setup', cause: err });
|
|
151
|
+
}
|
|
152
|
+
const referenceDoc = customReferenceDoc ?? generatedReferenceDoc?.path ?? DEFAULT_REFERENCE_DOC;
|
|
153
|
+
|
|
154
|
+
let inputPath;
|
|
155
|
+
if (typeof source === 'string') {
|
|
156
|
+
inputPath = join(work, 'input.md');
|
|
157
|
+
writeFileSync(inputPath, source, 'utf8');
|
|
158
|
+
} else {
|
|
159
|
+
inputPath = source.path;
|
|
160
|
+
}
|
|
161
|
+
const outputPath = join(work, 'output.docx');
|
|
162
|
+
|
|
163
|
+
const pandocArgs = [inputPath, '-o', outputPath, '--lua-filter', filterPath];
|
|
164
|
+
// Pandoc picks its reader from the extension and does not know `.qmd`; Quarto Markdown is Pandoc
|
|
165
|
+
// Markdown plus front matter and code chunks, so naming the reader is correct and drops its warning.
|
|
166
|
+
if (extname(inputPath).toLowerCase() === '.qmd') pandocArgs.push('--from', 'markdown');
|
|
167
|
+
if (existsSync(referenceDoc)) pandocArgs.push('--reference-doc', referenceDoc);
|
|
168
|
+
const tocDepth = Math.min(4, Math.max(2, Math.round(options.tocDepth ?? 3)));
|
|
169
|
+
if (options.toc) pandocArgs.push('--toc', `--toc-depth=${tocDepth}`);
|
|
170
|
+
|
|
171
|
+
// The bridge (one run per ```mermaid block, spawned by the Lua filter) hands SmartArt and chart parts
|
|
172
|
+
// back through these directories: a Lua filter cannot add package parts itself.
|
|
173
|
+
const smartArtDir = options.smartArt !== false ? mkdtempSync(join(work, 'smartart-')) : null;
|
|
174
|
+
const chartDir = nativeChartsEnv(options.nativeCharts) !== null ? mkdtempSync(join(work, 'chart-')) : null;
|
|
175
|
+
const env = pandocEnvironment(options, {
|
|
176
|
+
smartArtDir,
|
|
177
|
+
chartDir,
|
|
178
|
+
// Usable page area for the translator, whether or not the reference document needed a patch.
|
|
179
|
+
maxDrawingExtentEmu: customReferenceDoc ? null : resolveMaxDrawingExtentEmu(layout),
|
|
180
|
+
// The filter's "back to portrait" section needs the document's actual page geometry.
|
|
181
|
+
landscapeGeometry: layout.landscapeTables
|
|
182
|
+
? { pgSize: resolvePageSize(layout.pageSize, layout.orientation), margins: resolveMargins(layout.margins, layout.marginsCustomCm) }
|
|
183
|
+
: null,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const pandocStderr = await runPandoc(options.pandocBin ?? 'pandoc', pandocArgs, { cwd, env });
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Namespaces and drawing ids first: the SmartArt and chart injection read the fixed document.xml.
|
|
190
|
+
postProcessDocx(outputPath, { toc: Boolean(options.toc), emojiFont: options.emojiFont !== false });
|
|
191
|
+
if (smartArtDir) injectSmartArtParts(outputPath, smartArtDir);
|
|
192
|
+
if (chartDir) injectChartParts(outputPath, chartDir);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
throw new ConversionError(`post-processing failed: ${errorMessage(err)}`, { stage: 'postprocess', pandocStderr, cause: err });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { document: readFileSync(outputPath), warnings: extractWarnings(pandocStderr), pandocStderr };
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(work, { recursive: true, force: true });
|
|
200
|
+
if (generatedReferenceDoc) rmSync(generatedReferenceDoc.dir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `MD2NATIVEDOCX_*` environment variables, read into {@link convert}'s typed options.
|
|
3
|
+
*
|
|
4
|
+
* The variables stay the CLI's configuration interface: the VS Code extension's `exportService.ts` sets
|
|
5
|
+
* them from its settings, and a standalone user can set them by hand. Only this adapter reads them
|
|
6
|
+
* (docs/specs/01-v2-engine-spec.md §8); `convert()` itself never looks at the environment.
|
|
7
|
+
*
|
|
8
|
+
* Variables the CLI sets for the Lua filter and the bridge (`_SMARTART_DIR`, `_CHART_DIR`, `_NODE_BIN`,
|
|
9
|
+
* `_PAGE_*_TWIPS`, `_MARGIN_*_TWIPS`) are not inputs and are not read here.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
/** Whether any page/typography option is set (they are ignored under a custom reference document). */
|
|
15
|
+
export function hasLayoutOptions(layout) {
|
|
16
|
+
return Object.values(layout ?? {}).some((v) => v !== undefined);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {NodeJS.ProcessEnv} env
|
|
21
|
+
* @returns {import('./convert.d.mts').ConvertOptions}
|
|
22
|
+
*/
|
|
23
|
+
export function readConvertOptionsFromEnv(env) {
|
|
24
|
+
const str = (name) => {
|
|
25
|
+
const v = env[name];
|
|
26
|
+
return v && v.trim() !== '' ? v.trim() : undefined;
|
|
27
|
+
};
|
|
28
|
+
const num = (name) => {
|
|
29
|
+
const v = env[name];
|
|
30
|
+
if (v === undefined || v.trim() === '') return undefined;
|
|
31
|
+
const n = Number(v);
|
|
32
|
+
return Number.isFinite(n) ? n : undefined;
|
|
33
|
+
};
|
|
34
|
+
const positiveInt = (name) => {
|
|
35
|
+
const n = Number.parseInt(env[name] ?? '', 10);
|
|
36
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const marginsCustomCm = {
|
|
40
|
+
top: num('MD2NATIVEDOCX_MARGINS_CUSTOM_TOP'),
|
|
41
|
+
right: num('MD2NATIVEDOCX_MARGINS_CUSTOM_RIGHT'),
|
|
42
|
+
bottom: num('MD2NATIVEDOCX_MARGINS_CUSTOM_BOTTOM'),
|
|
43
|
+
left: num('MD2NATIVEDOCX_MARGINS_CUSTOM_LEFT'),
|
|
44
|
+
};
|
|
45
|
+
const layout = {
|
|
46
|
+
pageSize: str('MD2NATIVEDOCX_PAGE_SIZE'),
|
|
47
|
+
orientation: str('MD2NATIVEDOCX_ORIENTATION'),
|
|
48
|
+
margins: str('MD2NATIVEDOCX_MARGINS'),
|
|
49
|
+
marginsCustomCm: Object.values(marginsCustomCm).some((v) => v !== undefined) ? marginsCustomCm : undefined,
|
|
50
|
+
headingFont: str('MD2NATIVEDOCX_HEADING_FONT'),
|
|
51
|
+
bodyFont: str('MD2NATIVEDOCX_BODY_FONT'),
|
|
52
|
+
fontSizePt: num('MD2NATIVEDOCX_FONT_SIZE'),
|
|
53
|
+
lineSpacing: str('MD2NATIVEDOCX_LINE_SPACING'),
|
|
54
|
+
justify: str('MD2NATIVEDOCX_JUSTIFY'),
|
|
55
|
+
accentColor: str('MD2NATIVEDOCX_ACCENT_COLOR'),
|
|
56
|
+
tableHeaderColor: str('MD2NATIVEDOCX_TABLE_HEADER_COLOR'),
|
|
57
|
+
footerPageNumber: env.MD2NATIVEDOCX_FOOTER_PAGE_NUMBER === '1' ? true : undefined,
|
|
58
|
+
landscapeTables: env.MD2NATIVEDOCX_LANDSCAPE_TABLES === '1' ? true : undefined,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Unset or `1`: every chartable type; `0`: none; otherwise a comma-separated list of types.
|
|
62
|
+
const chartsRaw = (env.MD2NATIVEDOCX_NATIVE_CHARTS ?? '').trim();
|
|
63
|
+
const nativeCharts = chartsRaw === '' || chartsRaw === '1' ? true : chartsRaw === '0' ? false : chartsRaw.split(',').map((t) => t.trim());
|
|
64
|
+
|
|
65
|
+
const tocDepth = Number(env.MD2NATIVEDOCX_TOC_DEPTH);
|
|
66
|
+
// A reference document that does not exist is ignored (the bundled one is used), as it always was.
|
|
67
|
+
const referenceDoc = env.MD2NATIVEDOCX_REFERENCE_DOC && existsSync(env.MD2NATIVEDOCX_REFERENCE_DOC) ? env.MD2NATIVEDOCX_REFERENCE_DOC : undefined;
|
|
68
|
+
|
|
69
|
+
/** @type {import('./convert.d.mts').ConvertOptions} */
|
|
70
|
+
const options = {
|
|
71
|
+
layout,
|
|
72
|
+
toc: env.MD2NATIVEDOCX_TOC === '1',
|
|
73
|
+
emojiFont: env.MD2NATIVEDOCX_EMOJI_FONT !== '0',
|
|
74
|
+
// SmartArt and native charts are on by default, as in the VS Code extension; `0` turns them off.
|
|
75
|
+
smartArt: env.MD2NATIVEDOCX_ENABLE_SMARTART !== '0',
|
|
76
|
+
smartArtDrawing: env.MD2NATIVEDOCX_SMARTART_DRAWING !== '0',
|
|
77
|
+
nativeCharts,
|
|
78
|
+
};
|
|
79
|
+
if (Number.isFinite(tocDepth)) options.tocDepth = tocDepth;
|
|
80
|
+
if (referenceDoc) options.referenceDoc = referenceDoc;
|
|
81
|
+
if (env.MD2NATIVEDOCX_SMARTART_STYLE !== undefined) options.smartArtStyle = env.MD2NATIVEDOCX_SMARTART_STYLE;
|
|
82
|
+
const maxDrawingCx = positiveInt('MD2NATIVEDOCX_MAX_DRAWING_CX');
|
|
83
|
+
const maxDrawingCy = positiveInt('MD2NATIVEDOCX_MAX_DRAWING_CY');
|
|
84
|
+
if (maxDrawingCx !== undefined) options.maxDrawingCx = maxDrawingCx;
|
|
85
|
+
if (maxDrawingCy !== undefined) options.maxDrawingCy = maxDrawingCy;
|
|
86
|
+
if (env.MD2NATIVEDOCX_PANDOC_BIN) options.pandocBin = env.MD2NATIVEDOCX_PANDOC_BIN;
|
|
87
|
+
return options;
|
|
88
|
+
}
|