@md2nativedocx/cli 0.0.0-stage → 0.2.1

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
@@ -1,3 +1,115 @@
1
- # Temporary Holding Version
1
+ # @md2nativedocx/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Convert **Markdown to native Word (`.docx`)** — a complete document conversion (text, tables, footnotes, math)
4
+ in which every embedded **Mermaid** diagram becomes real Word objects (editable OOXML shapes, SmartArt, charts),
5
+ not flattened PNGs. (`md` = Markdown, the input; *native* describes the Word output.) Standalone CLI counterpart to the
6
+ [md2nativedocx VS Code extension](https://marketplace.visualstudio.com/items?itemName=md2nativedocx.md2nativedocx),
7
+ useful for CI pipelines, scripts, Git hooks, or any editor other than VS Code.
8
+
9
+ Full project: <https://github.com/nicolasbridelance/md2nativedocx>
10
+
11
+ ## Requirements
12
+
13
+ - Node.js 18+
14
+ - [Pandoc](https://pandoc.org) 3.1.3+ on `PATH` (or point `MD2NATIVEDOCX_PANDOC_BIN` at a specific
15
+ binary). Unlike the VS Code extension, this standalone CLI does not auto-download Pandoc for you.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm install -g @md2nativedocx/cli
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```sh
26
+ md2nativedocx report.md -o report.docx
27
+ ```
28
+
29
+ ```
30
+ Usage: md2nativedocx <input.md> -o <output.docx|output.odt|output.pptx> [options]
31
+
32
+ Options:
33
+ -o, --output <file> Output .docx, .odt or .pptx path (required)
34
+ --show-source .pptx only: show each diagram's Mermaid source beside it
35
+ -h, --help Show this help
36
+ ```
37
+
38
+ Every ` ```mermaid ` code block in the Markdown becomes native, editable Word objects, never an embedded
39
+ image: a SmartArt graphic when the diagram fits one (processes, cycles, org charts, mindmaps, timelines,
40
+ kanban boards…), a Word chart with editable data for `pie`, `xychart` and `radar`, and individually
41
+ selectable shapes otherwise. Everything else (headings, tables, lists, LaTeX math, ...) is handled by
42
+ Pandoc as usual. Per-type detail:
43
+ [`docs/coverage.md`](https://github.com/nicolasbridelance/md2nativedocx/blob/main/docs/coverage.md).
44
+
45
+ | Variable | Effect |
46
+ |---|---|
47
+ | `MD2NATIVEDOCX_ENABLE_SMARTART=0` | No SmartArt: every diagram as shapes |
48
+ | `MD2NATIVEDOCX_NATIVE_CHARTS=0` (or `pie,xychart`) | No Word charts (or only those types) |
49
+ | `MD2NATIVEDOCX_SMARTART_STYLE` | SmartArt look: `simple`, `subtle`, `moderate`, `intense-accent`, `colorful` (default), `colorful-moderate`, `intense` |
50
+
51
+ ### LibreOffice (`.odt`)
52
+
53
+ ```sh
54
+ md2nativedocx report.md -o report.odt
55
+ ```
56
+
57
+ Writes an OpenDocument text for LibreOffice: the document as Pandoc converts it, and each flowchart as one
58
+ group of native LibreOffice shapes (boxes, diamonds, cylinders…) with connectors attached to them, which
59
+ follow a box you move (*F3* enters the group), subgraphs as containers, `classDef`/`style`/`linkStyle`
60
+ colours kept. Other diagram types become a note in their place for now. The page is A4 with 2.54 cm
61
+ margins; the page and typography variables below, SmartArt and charts apply to `.docx` only. Pandoc
62
+ writes the whole file, with a reference document and template shipped in `assets/` (see
63
+ `THIRD_PARTY_NOTICES.md` for the template's BSD-3 notice).
64
+
65
+ From code: `convert(markdown, { format: 'odt' })`.
66
+
67
+ ## Configuration
68
+
69
+ Page layout, typography, table of contents, and other export options are set via environment
70
+ variables (the same ones the VS Code extension's settings map to) — see
71
+ [`docs/specs/export_customization_SPEC.md`](https://github.com/nicolasbridelance/md2nativedocx/blob/main/docs/specs/export_customization_SPEC.md)
72
+ in the main repository for the full list.
73
+
74
+ ## Exit codes
75
+
76
+ | Code | Meaning |
77
+ |---|---|
78
+ | `0` | Written. Diagram warnings, if any, are on stderr and in the `.log` next to the output. |
79
+ | `1` | No document: input file not found, Pandoc missing or failed, post-processing failed. |
80
+ | `2` | Bad command line: unknown option, missing input or `-o`, a path outside the working directory. |
81
+
82
+ ## Very large diagrams
83
+
84
+ A diagram with more than 500 nodes (subgraph boxes included) or 800 edges is not laid out: the document
85
+ still converts, the diagram is replaced by a grey note saying why, and a warning goes to stderr and the
86
+ `.log`. This keeps one pathological block from running for minutes. In `@md2nativedocx/core`, `renderDiagram()`
87
+ throws a `DiagramTooLargeError` instead, and takes `maxNodes`, `maxEdges` and `maxSourceLength` to change the limits.
88
+
89
+ ## From Node: `convert()`
90
+
91
+ The same conversion as a function, for a Node application, a build script or a server, without spawning
92
+ the CLI. Pandoc must still be installed.
93
+
94
+ ```js
95
+ import { writeFile } from 'node:fs/promises';
96
+ import { convert, ConversionError } from '@md2nativedocx/cli';
97
+
98
+ const { document, warnings } = await convert('# Report\n\n```mermaid\ngraph LR\n A --> B\n```\n', {
99
+ layout: { pageSize: 'A4', margins: 'narrow' },
100
+ toc: true,
101
+ });
102
+ await writeFile('report.docx', document);
103
+ ```
104
+
105
+ `convert()` takes Markdown text, or `{ path }` of a file, and resolves to `{ document, warnings,
106
+ pandocStderr }`; `document` is the `.docx` as a `Buffer`. It reads no environment variable: every setting
107
+ is an option (types in `src/convert.d.mts`, mirroring the variables above). A failure is a
108
+ `ConversionError` whose `stage` is `setup`, `pandoc` or `postprocess`. To render a single diagram without
109
+ Pandoc, use `renderDiagram()` from `@md2nativedocx/core`.
110
+
111
+ ## License
112
+
113
+ CC0-1.0 (public domain). See the [main repository](https://github.com/nicolasbridelance/md2nativedocx)
114
+ for compliance/licensing details, including third-party notices for Pandoc (GPL-2.0-or-later,
115
+ invoked as a separate subprocess, never bundled).
@@ -0,0 +1,38 @@
1
+ # Third-party notices
2
+
3
+ Everything in this package is CC0-1.0 except the file below.
4
+
5
+ ## `assets/md2nativedocx.opendocument`
6
+
7
+ Derived from Pandoc's `opendocument` template (Pandoc 3.1.3, `data/templates/default.opendocument`).
8
+ Pandoc's templates are dual-licensed under the GNU GPL (version 2 or later) and the BSD 3-Clause
9
+ license; this derived file is distributed under the BSD 3-Clause license. The only change is a loop
10
+ added inside `office:automatic-styles`, filled by this project's Lua filter with the styles of the
11
+ diagrams it draws. That change is distributed under the same license.
12
+
13
+ ```
14
+ Copyright (c) 2014-2023, John MacFarlane <jgm@berkeley.edu>
15
+ All rights reserved.
16
+
17
+ Redistribution and use in source and binary forms, with or without modification, are permitted
18
+ provided that the following conditions are met:
19
+
20
+ * Redistributions of source code must retain the above copyright notice, this list of conditions
21
+ and the following disclaimer.
22
+ * Redistributions in binary form must reproduce the above copyright notice, this list of
23
+ conditions and the following disclaimer in the documentation and/or other materials provided
24
+ with the distribution.
25
+ * Neither the name of John MacFarlane nor the names of other contributors may be used to endorse
26
+ or promote products derived from this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
29
+ IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
30
+ FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
31
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
32
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
34
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
35
+ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ ```
37
+
38
+ Pandoc itself is not part of this package: it is run as a separate program (GPL-2.0-or-later).
@@ -0,0 +1,65 @@
1
+ # `reference.docx`
2
+
3
+ Passed to Pandoc via `--reference-doc` so generated documents use Word's current default look
4
+ (Aptos font scheme, the modern "Office" colour scheme, accent1 `#4472C4`, flat non-bold heading
5
+ hierarchy, 11 pt body text with 1.08 line spacing and 8 pt after) instead of Pandoc's own bundled
6
+ default, which is the 2007-2010 Office look (Calibri/Cambria, bold coloured headings).
7
+
8
+ **Written by this project, from scratch (2026-10-08).** Every part comes from
9
+ `packages/cli/reference-docx-src/` and is assembled by:
10
+
11
+ ```bash
12
+ node scripts/build-reference-docx.mjs
13
+ ```
14
+
15
+ Edit a part there, rebuild, and commit both. The build is reproducible (fixed timestamps).
16
+
17
+ Why: the previous file was derived from Pandoc's default `reference.docx`, which Pandoc distributes
18
+ under GPL-2+ only (its templates are dual GPL-2+/BSD-3, its other data files are not), while this
19
+ project is CC0. The maintainer chose to rebuild it (ADR 0013, `docs/adr/0013-odf-output-phase-0.md`).
20
+ The new parts keep the same visual values: a LibreOffice render of a document using every Pandoc
21
+ style (title block, abstract, headings, lists, footnote, code, table with caption, definition list,
22
+ diagram) is pixel-identical to the previous one. They contain no sample text and no relationship to
23
+ anything outside the package (the old file carried an unused `http://example.com` link from
24
+ Pandoc's sample document).
25
+
26
+ What the rest of the code relies on (`src/referenceDocBuilder.mjs`, tested in
27
+ `test/reference-doc-builder.test.mjs`):
28
+
29
+ - `word/theme/theme1.xml`: `<a:majorFont><a:latin typeface="…" />`, the same for `minorFont`, and
30
+ `<a:accent1><a:srgbClr val="RRGGBB"/></a:accent1>`;
31
+ - `word/styles.xml`: `w:docDefaults` with `w:sz`/`w:szCs` and a `w:pPrDefault`; accent colours as
32
+ `w:themeColor="accent1"` with a literal `w:val` fallback (LibreOffice does not resolve theme
33
+ colours); a `Table` style with a `firstRow` conditional `w:tcPr`;
34
+ - `word/document.xml`: an empty `<w:sectPr />`, which `patchSectPr` fills.
35
+
36
+ Styles Pandoc adds at conversion time (syntax highlighting `SourceCode`, `*Tok`) are not in this
37
+ file: Pandoc writes them itself.
38
+
39
+ **To verify in a real Word** (no Word in this environment; checked with LibreOffice and the Open XML
40
+ validator): heading styling and whether Word's current built-in template still matches.
41
+
42
+ # `reference.odt` and `md2nativedocx.opendocument`
43
+
44
+ The two files handed to Pandoc for a `.odt` (ADR 0013; `AGENTS.md` rule 7: nothing touches the
45
+ `.odt` Pandoc writes). Both are built by:
46
+
47
+ ```bash
48
+ node scripts/build-odt-assets.mjs
49
+ ```
50
+
51
+ - **`reference.odt`**, written by this project (CC0) from `packages/cli/reference-odt-src/`, never
52
+ Pandoc's own (GPL-2+). Pandoc keeps its `styles.xml` and writes the rest. It defines the styles
53
+ Pandoc's ODT writer refers to (headings, title block, code, quotations, tables, captions,
54
+ definition lists, footnotes, links), an A4 page with 2.54 cm margins, and the arrow markers and
55
+ dash pattern the diagrams use (`ODF_GRAPHIC_DEFINITIONS` in `@md2nativedocx/core`, kept equal by
56
+ `test/odt.test.mjs`). LibreOffice does **not** supply its built-in look for a style a document
57
+ references without defining (headings came out as plain text with a minimal `styles.xml`,
58
+ 2026-10-08), so each one is written here.
59
+ - **`md2nativedocx.opendocument`**, Pandoc 3.1.3's `opendocument` template plus one loop inside
60
+ `office:automatic-styles` for the diagram styles. BSD-3-Clause, see `../THIRD_PARTY_NOTICES.md`.
61
+ The build refuses any other Pandoc version, and `test/odt.test.mjs` checks the file still differs
62
+ from `pandoc -D opendocument` by that loop only.
63
+
64
+ Known Pandoc 3.1.3 limitation, not ours: a front-matter `abstract` is written as bare text outside
65
+ any paragraph, which the ODF validator rejects (same with Pandoc's own reference document).
@@ -0,0 +1,171 @@
1
+ $-- md2nativedocx: Pandoc's opendocument template (Pandoc 3.1.3), with one addition: the
2
+ $-- md2n-automatic-styles loop in office:automatic-styles, filled by the md2nativedocx Lua filter.
3
+ $-- Copyright (c) 2014-2023, John MacFarlane. BSD-3-Clause, see THIRD_PARTY_NOTICES.md.
4
+ <?xml version="1.0" encoding="utf-8" ?>
5
+ <office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" office:version="1.3">
6
+ <office:font-face-decls>
7
+ <style:font-face style:name="Courier New" style:font-family-generic="modern" style:font-pitch="fixed" svg:font-family="'Courier New'" />
8
+ </office:font-face-decls>
9
+ <office:automatic-styles>
10
+ $automatic-styles$
11
+ $for(md2n-automatic-styles)$
12
+ $md2n-automatic-styles$
13
+ $endfor$
14
+ </office:automatic-styles>
15
+ $for(header-includes)$
16
+ $header-includes$
17
+ $endfor$
18
+ <office:body>
19
+ <office:text>
20
+ $if(title)$
21
+ <text:p text:style-name="Title">$title$</text:p>
22
+ $endif$
23
+ $if(subtitle)$
24
+ <text:p text:style-name="Subtitle">$subtitle$</text:p>
25
+ $endif$
26
+ $for(author)$
27
+ <text:p text:style-name="Author">$author$</text:p>
28
+ $endfor$
29
+ $if(date)$
30
+ <text:p text:style-name="Date">$date$</text:p>
31
+ $endif$
32
+ $if(abstract)$
33
+ $abstract$
34
+ $endif$
35
+ $for(include-before)$
36
+ $include-before$
37
+ $endfor$
38
+ $if(toc)$
39
+ <text:table-of-content>
40
+ <text:table-of-content-source text:outline-level="$toc-depth$">
41
+ <text:index-title-template text:style-name="Contents_20_Heading">$toc-title$</text:index-title-template>
42
+ <text:table-of-content-entry-template text:outline-level="1"
43
+ text:style-name="Contents_20_1">
44
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
45
+ <text:index-entry-chapter />
46
+ <text:index-entry-text />
47
+ <text:index-entry-link-end />
48
+ <text:index-entry-tab-stop style:type="right"
49
+ style:leader-char="." />
50
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
51
+ <text:index-entry-page-number />
52
+ <text:index-entry-link-end />
53
+ </text:table-of-content-entry-template>
54
+ <text:table-of-content-entry-template text:outline-level="2"
55
+ text:style-name="Contents_20_2">
56
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
57
+ <text:index-entry-chapter />
58
+ <text:index-entry-text />
59
+ <text:index-entry-link-end />
60
+ <text:index-entry-tab-stop style:type="right"
61
+ style:leader-char="." />
62
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
63
+ <text:index-entry-page-number />
64
+ <text:index-entry-link-end />
65
+ </text:table-of-content-entry-template>
66
+ <text:table-of-content-entry-template text:outline-level="3"
67
+ text:style-name="Contents_20_3">
68
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
69
+ <text:index-entry-chapter />
70
+ <text:index-entry-text />
71
+ <text:index-entry-link-end />
72
+ <text:index-entry-tab-stop style:type="right"
73
+ style:leader-char="." />
74
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
75
+ <text:index-entry-page-number />
76
+ <text:index-entry-link-end />
77
+ </text:table-of-content-entry-template>
78
+ <text:table-of-content-entry-template text:outline-level="4"
79
+ text:style-name="Contents_20_4">
80
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
81
+ <text:index-entry-chapter />
82
+ <text:index-entry-text />
83
+ <text:index-entry-link-end />
84
+ <text:index-entry-tab-stop style:type="right"
85
+ style:leader-char="." />
86
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
87
+ <text:index-entry-page-number />
88
+ <text:index-entry-link-end />
89
+ </text:table-of-content-entry-template>
90
+ <text:table-of-content-entry-template text:outline-level="5"
91
+ text:style-name="Contents_20_5">
92
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
93
+ <text:index-entry-chapter />
94
+ <text:index-entry-text />
95
+ <text:index-entry-link-end />
96
+ <text:index-entry-tab-stop style:type="right"
97
+ style:leader-char="." />
98
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
99
+ <text:index-entry-page-number />
100
+ <text:index-entry-link-end />
101
+ </text:table-of-content-entry-template>
102
+ <text:table-of-content-entry-template text:outline-level="6"
103
+ text:style-name="Contents_20_6">
104
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
105
+ <text:index-entry-chapter />
106
+ <text:index-entry-text />
107
+ <text:index-entry-link-end />
108
+ <text:index-entry-tab-stop style:type="right"
109
+ style:leader-char="." />
110
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
111
+ <text:index-entry-page-number />
112
+ <text:index-entry-link-end />
113
+ </text:table-of-content-entry-template>
114
+ <text:table-of-content-entry-template text:outline-level="7"
115
+ text:style-name="Contents_20_7">
116
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
117
+ <text:index-entry-chapter />
118
+ <text:index-entry-text />
119
+ <text:index-entry-link-end />
120
+ <text:index-entry-tab-stop style:type="right"
121
+ style:leader-char="." />
122
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
123
+ <text:index-entry-page-number />
124
+ <text:index-entry-link-end />
125
+ </text:table-of-content-entry-template>
126
+ <text:table-of-content-entry-template text:outline-level="8"
127
+ text:style-name="Contents_20_8">
128
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
129
+ <text:index-entry-chapter />
130
+ <text:index-entry-text />
131
+ <text:index-entry-link-end />
132
+ <text:index-entry-tab-stop style:type="right"
133
+ style:leader-char="." />
134
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
135
+ <text:index-entry-page-number />
136
+ <text:index-entry-link-end />
137
+ </text:table-of-content-entry-template>
138
+ <text:table-of-content-entry-template text:outline-level="9"
139
+ text:style-name="Contents_20_9">
140
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
141
+ <text:index-entry-chapter />
142
+ <text:index-entry-text />
143
+ <text:index-entry-link-end />
144
+ <text:index-entry-tab-stop style:type="right"
145
+ style:leader-char="." />
146
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
147
+ <text:index-entry-page-number />
148
+ <text:index-entry-link-end />
149
+ </text:table-of-content-entry-template>
150
+ <text:table-of-content-entry-template text:outline-level="10"
151
+ text:style-name="Contents_20_10">
152
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
153
+ <text:index-entry-chapter />
154
+ <text:index-entry-text />
155
+ <text:index-entry-link-end />
156
+ <text:index-entry-tab-stop style:type="right"
157
+ style:leader-char="." />
158
+ <text:index-entry-link-start text:style-name="Internet_20_link" />
159
+ <text:index-entry-page-number />
160
+ <text:index-entry-link-end />
161
+ </text:table-of-content-entry-template>
162
+ </text:table-of-content-source>
163
+ </text:table-of-content>
164
+ $endif$
165
+ $body$
166
+ $for(include-after)$
167
+ $include-after$
168
+ $endfor$
169
+ </office:text>
170
+ </office:body>
171
+ </office:document-content>
Binary file
Binary file
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * md2nativedocx CLI.
4
+ *
5
+ * `npx md2nativedocx rapport.md -o rapport.docx` packages the Pandoc invocation
6
+ * with the md2nativedocx Lua filter, producing a .docx with native, editable
7
+ * OOXML vector shapes for every ```mermaid block — or, for a flowchart shape
8
+ * `classifyTopology()` accepts (chain/tree/cycle, spec §4), an editable native
9
+ * SmartArt diagram instead (spec §7 step 5). See postprocess.mjs's
10
+ * `injectSmartArtParts` doc comment for how that dispatch is wired end to end.
11
+ *
12
+ * The conversion itself is `convert()` (src/convert.mjs), the package's
13
+ * library entry point; this file only reads the `MD2NATIVEDOCX_*` variables
14
+ * into its options (src/envOptions.mjs), handles files and arguments, and
15
+ * reports warnings, errors and the Word compatibility check.
16
+ *
17
+ * Security (AGENTS.md):
18
+ * * Rule #4: Pandoc is invoked via execFile with an argument array — never a
19
+ * shell string that interpolates a file path (src/convert.mjs).
20
+ * * Path traversal: input/output paths are resolved and validated against the
21
+ * expected root before any file operation.
22
+ * * Errors are typed (ParseError/TranslationError) and mapped to exit codes.
23
+ */
24
+
25
+ import { basename, extname } from 'node:path';
26
+ import { existsSync } from 'node:fs';
27
+ import { CliError, resolveSafePath } from '../src/cliSupport.mjs';
28
+ import { ConversionError, convert } from '../src/convert.mjs';
29
+ import { writeOutputSafely } from '../src/writeOutputSafely.mjs';
30
+ import { formatWordCompatibility, runWordCompatibilityCheck } from '../src/wordCheck.mjs';
31
+ import { hasLayoutOptions, readConvertOptionsFromEnv } from '../src/envOptions.mjs';
32
+
33
+ // `-o something.pptx` takes the Pandoc-free deck path (packages/pptx), dispatched before anything a
34
+ // .docx export needs.
35
+ {
36
+ const argv = process.argv.slice(2);
37
+ const outIndex = argv.findIndex((a) => a === '-o' || a === '--output');
38
+ if (outIndex !== -1 && /\.pptx$/i.test(argv[outIndex + 1] ?? '')) {
39
+ const { runPptxCli } = await import('../src/pptxExport.mjs');
40
+ process.exit(await runPptxCli(argv, process.cwd()));
41
+ }
42
+ }
43
+
44
+ const USAGE = `Usage: md2nativedocx <input.md> -o <output.docx|output.odt|output.pptx> [options]
45
+
46
+ A .odt output is for LibreOffice: flowcharts become native, editable shapes and connectors;
47
+ other diagram types become a note for now.
48
+
49
+ A .pptx output is a slide deck: one slide per \`\`\`mermaid block, titled with the nearest
50
+ preceding heading (text outside diagrams is not exported).
51
+
52
+ Options:
53
+ -o, --output <file> Output .docx, .odt or .pptx path (required)
54
+ --show-source .pptx only: show each diagram's Mermaid source beside it
55
+ -h, --help Show this help
56
+ `;
57
+
58
+ function parseArgs(argv) {
59
+ const args = { input: null, output: null };
60
+ for (let i = 0; i < argv.length; i++) {
61
+ const a = argv[i];
62
+ if (a === '-h' || a === '--help') {
63
+ args.help = true;
64
+ } else if (a === '-o' || a === '--output') {
65
+ args.output = argv[++i];
66
+ if (!args.output) throw new CliError('Missing value for --output', 2);
67
+ } else if (a.startsWith('-')) {
68
+ throw new CliError(`Unknown option: ${a}`, 2);
69
+ } else if (args.input === null) {
70
+ args.input = a;
71
+ } else {
72
+ throw new CliError(`Unexpected argument: ${a}`, 2);
73
+ }
74
+ }
75
+ return args;
76
+ }
77
+
78
+ async function main() {
79
+ const cwd = process.cwd();
80
+ const options = readConvertOptionsFromEnv(process.env);
81
+ // A custom reference document wins over page/typography options (spec §2.1, option (a)). Said as an
82
+ // info line, not with the `md2nativedocx: ` prefix that counts as a warning: nothing is wrong with
83
+ // the export, the user should just know.
84
+ if (options.referenceDoc && hasLayoutOptions(options.layout)) {
85
+ process.stderr.write(
86
+ 'md2nativedocx (info): page/typography options are ignored because a custom reference document (MD2NATIVEDOCX_REFERENCE_DOC) is set.\n',
87
+ );
88
+ }
89
+
90
+ let args;
91
+ try {
92
+ args = parseArgs(process.argv.slice(2));
93
+ } catch (err) {
94
+ if (err instanceof CliError) {
95
+ process.stderr.write(`${err.message}\n${USAGE}`);
96
+ process.exit(err.exitCode);
97
+ }
98
+ throw err;
99
+ }
100
+
101
+ if (args.help) {
102
+ process.stdout.write(USAGE);
103
+ process.exit(0);
104
+ }
105
+ if (!args.input) {
106
+ process.stderr.write(`Missing input file.\n${USAGE}`);
107
+ process.exit(2);
108
+ }
109
+ if (!args.output) {
110
+ process.stderr.write(`Missing output file (-o).\n${USAGE}`);
111
+ process.exit(2);
112
+ }
113
+
114
+ // Validate paths (anti path traversal): a path outside the working directory is a usage error (exit 2),
115
+ // as on the .pptx path.
116
+ let input;
117
+ let output;
118
+ try {
119
+ input = resolveSafePath(args.input, cwd);
120
+ output = resolveSafePath(args.output, cwd);
121
+ } catch (err) {
122
+ if (err instanceof CliError) {
123
+ process.stderr.write(`${err.message}\n`);
124
+ process.exit(err.exitCode);
125
+ }
126
+ throw err;
127
+ }
128
+ if (!existsSync(input)) {
129
+ process.stderr.write(`Input file not found: ${args.input}\n`);
130
+ process.exit(1);
131
+ }
132
+
133
+ const format = extname(output).toLowerCase() === '.odt' ? 'odt' : 'docx';
134
+ if (format === 'odt' && hasLayoutOptions(options.layout)) {
135
+ process.stderr.write('md2nativedocx (info): page/typography options apply to .docx only; the .odt uses A4 with 2.54 cm margins.\n');
136
+ }
137
+ // MD2NATIVEDOCX_REFERENCE_DOC names a Word template; a .odt keeps the bundled reference.odt.
138
+ const formatOptions = format === 'odt' && options.referenceDoc && extname(options.referenceDoc).toLowerCase() !== '.odt'
139
+ ? { ...options, referenceDoc: undefined }
140
+ : options;
141
+
142
+ let result;
143
+ try {
144
+ result = await convert({ path: input }, { ...formatOptions, cwd, format });
145
+ } catch (err) {
146
+ if (err instanceof ConversionError) {
147
+ process.stderr.write(`md2nativedocx: ${err.message}\n`);
148
+ // `exportService.ts` (VS Code) reads Pandoc's own message, e.g. to tell a missing Pandoc apart.
149
+ if (err.stage === 'pandoc' && err.pandocStderr) process.stderr.write(err.pandocStderr);
150
+ process.exit(1);
151
+ }
152
+ throw err;
153
+ }
154
+ try {
155
+ writeOutputSafely(output, result.document);
156
+ } catch (err) {
157
+ if (err instanceof ConversionError) {
158
+ process.stderr.write(`md2nativedocx: ${err.message}\n`);
159
+ process.exit(2);
160
+ }
161
+ throw err;
162
+ }
163
+
164
+ // Surface the diagrams' non-fatal notices: never let a successful export silently hide something the
165
+ // author should know about.
166
+ const warnings = result.warnings.map((w) => `md2nativedocx: ${w}`);
167
+ const wordCompatibility = format === 'docx' ? runWordCompatibilityCheck(output) : null;
168
+ const logPath = writeExportLog({ input, output, warnings, rawStderr: result.pandocStderr, wordCompatibility });
169
+ if (warnings.length > 0) {
170
+ process.stdout.write(`Warnings: ${warnings.length} (see ${basename(logPath)})\n`);
171
+ for (const warning of warnings) process.stderr.write(`${warning}\n`);
172
+ }
173
+ process.stdout.write(`Wrote ${basename(output)}\n`);
174
+ }
175
+
176
+ /** Write a plain-text log next to the output .docx (same basename, `.log`
177
+ * extension) so a click-right export (no VS Code Problems panel in view) has
178
+ * somewhere durable to point to — spec §10, "surface warnings". Written on
179
+ * every successful export, not just when there are warnings, for a
180
+ * consistent, discoverable location. */
181
+ function writeExportLog({ input, output, warnings, rawStderr, wordCompatibility }) {
182
+ const logPath = ['.docx', '.odt'].includes(extname(output).toLowerCase())
183
+ ? output.slice(0, -extname(output).length) + '.log'
184
+ : `${output}.log`;
185
+ const lines = [
186
+ 'md2nativedocx export log',
187
+ `Date: ${new Date().toISOString()}`,
188
+ `Input: ${input}`,
189
+ `Output: ${output}`,
190
+ `Warnings: ${warnings.length}`,
191
+ '',
192
+ ];
193
+ if (warnings.length > 0) {
194
+ lines.push(...warnings.map((w) => `- ${w.replace(/^md2nativedocx:\s*(warning:\s*)?/, '')}`), '');
195
+ } else {
196
+ lines.push('No warnings.', '');
197
+ }
198
+ lines.push('--- Word compatibility check ---', ...formatWordCompatibility(wordCompatibility), '');
199
+ if (rawStderr && rawStderr.trim().length > 0) {
200
+ lines.push('--- Raw Pandoc/tool output ---', rawStderr.trimEnd(), '');
201
+ }
202
+ writeOutputSafely(logPath, lines.join('\n'));
203
+ return logPath;
204
+ }
205
+
206
+ main().catch((err) => {
207
+ process.stderr.write(`md2nativedocx: ${err instanceof Error ? err.message : String(err)}\n`);
208
+ process.exit(1);
209
+ });