word-to-markdown 0.4.0 → 0.5.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 +8 -1
- package/build/cli.js +11 -1
- package/build/cli.js.map +1 -1
- package/build/main.d.ts +3 -3
- package/build/main.js +231 -166
- package/build/main.js.map +1 -1
- package/package.json +37 -27
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Word to Markdown
|
|
1
|
+
# Word to Markdown (DOCX → Markdown converter)
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/word-to-markdown)
|
|
4
4
|
[](https://www.npmjs.com/package/word-to-markdown)
|
|
@@ -206,6 +206,13 @@ To self-host the static site using Docker Compose:
|
|
|
206
206
|
3. Run `docker compose up -d`
|
|
207
207
|
4. Access at http://localhost:3000
|
|
208
208
|
|
|
209
|
+
This serves `dist/` with plain nginx, so it skips what the Cloudflare Worker and
|
|
210
|
+
`public/_headers` add in production: the `Accept-Language` redirect on `/`, the
|
|
211
|
+
`/api/event` counter (requests to it 404), the security headers, and long-lived
|
|
212
|
+
caching for `/_astro/*`. Conversion itself runs in the browser and works the
|
|
213
|
+
same. For a production-like local setup, run `npm run build` and then
|
|
214
|
+
`npx wrangler dev`.
|
|
215
|
+
|
|
209
216
|
## More context
|
|
210
217
|
|
|
211
218
|
See the README of [the original Word to Markdown](https://github.com/benbalter/word-to-markdown?tab=readme-ov-file#the-problem) for the project's motivation.
|
package/build/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ program
|
|
|
26
26
|
.option('--underline', 'Preserve underlined text as inline <u> tags (dropped by default)')
|
|
27
27
|
.option('--preserve-footnotes', 'Keep Word footnotes as raw <sup> links and a numbered note list instead ' +
|
|
28
28
|
'of converting them to GFM [^1] footnotes')
|
|
29
|
+
.option('--verbose', 'On failure, also print the underlying error and stack trace')
|
|
29
30
|
.action(async (file, options) => {
|
|
30
31
|
try {
|
|
31
32
|
// --image-dir (extract) takes precedence over --strip-images.
|
|
@@ -64,7 +65,8 @@ program
|
|
|
64
65
|
// Write the Markdown to the requested file, or stdout by default.
|
|
65
66
|
if (options.output) {
|
|
66
67
|
await mkdir(path.dirname(options.output), { recursive: true });
|
|
67
|
-
|
|
68
|
+
// End with a newline, as stdout (console.log) does (markdownlint MD047).
|
|
69
|
+
await writeFile(options.output, `${result.markdown}\n`);
|
|
68
70
|
console.error(`Wrote Markdown to ${options.output}`);
|
|
69
71
|
}
|
|
70
72
|
else {
|
|
@@ -74,6 +76,14 @@ program
|
|
|
74
76
|
catch (error) {
|
|
75
77
|
// Converter errors carry user-friendly messages; print anything else as-is
|
|
76
78
|
console.error('Error:', error instanceof Error ? error.message : String(error));
|
|
79
|
+
// The friendly message hides the root cause (e.g. a ConversionError
|
|
80
|
+
// wrapping a mammoth failure); --verbose shows it for bug reports.
|
|
81
|
+
if (options.verbose && error instanceof Error) {
|
|
82
|
+
console.error('');
|
|
83
|
+
console.error(error.stack);
|
|
84
|
+
if (error.cause)
|
|
85
|
+
console.error('Caused by:', error.cause);
|
|
86
|
+
}
|
|
77
87
|
process.exit(1);
|
|
78
88
|
}
|
|
79
89
|
});
|
package/build/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { createRequire } from 'module';\nimport { mkdir, writeFile } from 'fs/promises';\nimport path from 'path';\nimport { convertWithWarnings } from './main.js';\n\n// Read our own version from package.json. createRequire resolves relative to\n// this module, so `../package.json` points at the package root both from the\n// TypeScript source (src/) and the compiled CLI (build/) after publish. This\n// avoids JSON import attributes, which node16 module resolution would require.\nconst require = createRequire(import.meta.url);\nconst { version } = require('../package.json') as { version: string };\n\nconst program = new Command();\nprogram.name('w2m');\nprogram.description('Convert Word documents to beautiful Markdown');\nprogram.version(version);\nprogram\n .command('convert', { isDefault: true })\n .argument('<file>', 'The Word document to convert')\n .option(\n '-o, --output <file>',\n 'Write the Markdown to <file> instead of stdout. Warnings still print to ' +\n 'stderr.',\n )\n .option(\n '--strip-images',\n 'Remove images instead of embedding them as base64 data URIs',\n )\n .option(\n '--image-dir <dir>',\n 'Extract images to <dir> and link them relatively, instead of embedding ' +\n 'them as base64. Links resolve relative to where you save the Markdown.',\n )\n .option(\n '--bullet-lists',\n 'Convert numbered lists to bullets rather than keeping them as 1./2./3.',\n )\n .option(\n '--underline',\n 'Preserve underlined text as inline <u> tags (dropped by default)',\n )\n .option(\n '--preserve-footnotes',\n 'Keep Word footnotes as raw <sup> links and a numbered note list instead ' +\n 'of converting them to GFM [^1] footnotes',\n )\n .option(\n '--verbose',\n 'On failure, also print the underlying error and stack trace',\n )\n .action(async (file, options) => {\n try {\n // --image-dir (extract) takes precedence over --strip-images.\n if (options.imageDir && options.stripImages) {\n console.error('Ignoring --strip-images because --image-dir is set.');\n }\n const images = options.imageDir\n ? 'extract'\n : options.stripImages\n ? 'strip'\n : 'inline';\n const result = await convertWithWarnings(file, {\n images,\n imageDir: options.imageDir,\n numberedLists: options.bulletLists ? 'bullets' : 'ordered',\n underline: options.underline ? 'preserve' : 'ignore',\n footnotes: options.preserveFootnotes ? 'preserve' : 'gfm',\n });\n\n // Write extracted images to disk before emitting the Markdown that links\n // them. The links are relative to the Markdown file, so resolve them\n // against its directory (the working directory when writing to stdout).\n if (result.images && result.images.length > 0) {\n const markdownDir = options.output ? path.dirname(options.output) : '.';\n const imageDir = path.resolve(markdownDir, options.imageDir);\n await mkdir(imageDir, { recursive: true });\n await Promise.all(\n result.images.map((image) =>\n writeFile(path.resolve(markdownDir, image.path), image.bytes),\n ),\n );\n console.error(\n `Wrote ${result.images.length} image(s) to ${path.relative('.', imageDir) || '.'}/`,\n );\n }\n\n // Display warnings to stderr if any\n if (result.warnings.length > 0) {\n result.warnings.forEach((warning) => {\n console.error(warning);\n });\n console.error(''); // Empty line for separation\n }\n\n // Write the Markdown to the requested file, or stdout by default.\n if (options.output) {\n await mkdir(path.dirname(options.output), { recursive: true });\n // End with a newline, as stdout (console.log) does (markdownlint MD047).\n await writeFile(options.output, `${result.markdown}\\n`);\n console.error(`Wrote Markdown to ${options.output}`);\n } else {\n console.log(result.markdown);\n }\n } catch (error) {\n // Converter errors carry user-friendly messages; print anything else as-is\n console.error(\n 'Error:',\n error instanceof Error ? error.message : String(error),\n );\n // The friendly message hides the root cause (e.g. a ConversionError\n // wrapping a mammoth failure); --verbose shows it for bug reports.\n if (options.verbose && error instanceof Error) {\n console.error('');\n console.error(error.stack);\n if (error.cause) console.error('Caused by:', error.cause);\n }\n process.exit(1);\n }\n });\n\nawait program.parseAsync();\n"]}
|
package/build/main.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export interface DocumentProperties {
|
|
|
57
57
|
protection?: boolean;
|
|
58
58
|
}
|
|
59
59
|
export declare class WordToMarkdownError extends Error {
|
|
60
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
60
61
|
}
|
|
61
62
|
export declare class UnsupportedFileError extends WordToMarkdownError {
|
|
62
63
|
constructor(message: string);
|
|
@@ -65,7 +66,7 @@ export declare class FileNotFoundError extends WordToMarkdownError {
|
|
|
65
66
|
constructor(filePath?: string);
|
|
66
67
|
}
|
|
67
68
|
export declare class InvalidFileError extends WordToMarkdownError {
|
|
68
|
-
constructor(filePath?: string);
|
|
69
|
+
constructor(filePath?: string, cause?: unknown);
|
|
69
70
|
}
|
|
70
71
|
export declare class FilePermissionError extends WordToMarkdownError {
|
|
71
72
|
constructor(filePath?: string);
|
|
@@ -74,14 +75,13 @@ export declare class ConversionError extends WordToMarkdownError {
|
|
|
74
75
|
constructor(message: string, originalError?: Error);
|
|
75
76
|
}
|
|
76
77
|
export declare function validateFileExtension(filePath: string): void;
|
|
77
|
-
export declare function htmlToMd(html: string, options?: object, keepTags?: string[]): string;
|
|
78
|
+
export declare function htmlToMd(html: string, options?: object, keepTags?: string[], gfmFootnotes?: boolean): string;
|
|
78
79
|
export declare function extractDocumentProperties(input: string | ArrayBuffer): Promise<DocumentProperties>;
|
|
79
80
|
export declare function generateWarnings(properties: DocumentProperties): string[];
|
|
80
81
|
interface MammothMessage {
|
|
81
82
|
type: string;
|
|
82
83
|
message: string;
|
|
83
84
|
}
|
|
84
|
-
export declare function extensionForContentType(contentType: string): string;
|
|
85
85
|
export declare function extractMammothWarnings(messages: readonly MammothMessage[]): string[];
|
|
86
86
|
export declare function convertWithWarnings(input: string | ArrayBuffer, options?: ConvertOptions): Promise<ConvertResult>;
|
|
87
87
|
export default function convert(input: string | ArrayBuffer, options?: ConvertOptions): Promise<string>;
|
package/build/main.js
CHANGED
|
@@ -12,6 +12,10 @@ import path from 'path';
|
|
|
12
12
|
// Base class for every user-facing error the converter throws, so callers can
|
|
13
13
|
// catch them all with one instanceof check
|
|
14
14
|
export class WordToMarkdownError extends Error {
|
|
15
|
+
constructor(message, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = new.target.name;
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
// Custom error class for unsupported file formats
|
|
17
21
|
export class UnsupportedFileError extends WordToMarkdownError {
|
|
@@ -30,9 +34,9 @@ export class FileNotFoundError extends WordToMarkdownError {
|
|
|
30
34
|
}
|
|
31
35
|
// Custom error class for invalid/corrupted files
|
|
32
36
|
export class InvalidFileError extends WordToMarkdownError {
|
|
33
|
-
constructor(filePath) {
|
|
37
|
+
constructor(filePath, cause) {
|
|
34
38
|
const location = filePath ? `: "${filePath}"` : '';
|
|
35
|
-
super(`Invalid file${location}. The file is not a valid .docx file or is corrupted. Please ensure the file is a valid Microsoft Word document (.docx format)
|
|
39
|
+
super(`Invalid file${location}. The file is not a valid .docx file or is corrupted. Please ensure the file is a valid Microsoft Word document (.docx format).`, cause === undefined ? undefined : { cause });
|
|
36
40
|
this.name = 'InvalidFileError';
|
|
37
41
|
}
|
|
38
42
|
}
|
|
@@ -67,36 +71,17 @@ export function validateFileExtension(filePath) {
|
|
|
67
71
|
throw new UnsupportedFileError('This tool only supports .docx files, not .doc files. Please save your document as a .docx file and try again.');
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
// Check for absolute paths to dangerous system directories (Unix-like systems)
|
|
81
|
-
const dangerousPaths = ['/etc/', '/sys/', '/proc/', '/root/', '/boot/'];
|
|
82
|
-
for (const dangerousPath of dangerousPaths) {
|
|
83
|
-
if (resolvedPath.startsWith(dangerousPath)) {
|
|
84
|
-
throw new FilePermissionError(filePath);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Check for Windows system directories
|
|
88
|
-
const windowsDangerousPaths = ['C:\\Windows\\', 'C:\\Program Files\\'];
|
|
89
|
-
for (const dangerousPath of windowsDangerousPaths) {
|
|
90
|
-
if (resolvedPath.toUpperCase().startsWith(dangerousPath.toUpperCase())) {
|
|
91
|
-
throw new FilePermissionError(filePath);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return resolvedPath;
|
|
74
|
+
// Read a .docx from disk into a standalone ArrayBuffer (Node.js only). The
|
|
75
|
+
// path is resolved as-is: a local caller already has full filesystem access, so
|
|
76
|
+
// there's nothing to sandbox. Paths containing `..` (`../report.docx`,
|
|
77
|
+
// `notes..docx`) are legitimate, and the OS enforces permissions (EACCES maps
|
|
78
|
+
// to FilePermissionError in classifyConversionError).
|
|
79
|
+
async function readFileBytes(filePath) {
|
|
80
|
+
const fileBuffer = await fs.readFile(path.resolve(filePath));
|
|
81
|
+
// Copy out of Node's pooled Buffer so the bytes don't alias unrelated data.
|
|
82
|
+
// readFile never returns a SharedArrayBuffer-backed Buffer, hence the cast.
|
|
83
|
+
return fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength);
|
|
95
84
|
}
|
|
96
|
-
// Turndown will add an empty header if the first row
|
|
97
|
-
// of the table isn't `<th>` elements. This function
|
|
98
|
-
// converts the first row of a table to `<th>` elements
|
|
99
|
-
// so that it renders correctly in Markdown.
|
|
100
85
|
// Common unicode bullets that might appear in Word documents - compiled once
|
|
101
86
|
const unicodeBullets = ['•', '◦', '▪', '▫', '‣', '⁃', '∙', '·'];
|
|
102
87
|
const bulletRegex = new RegExp(`^\\s*[${unicodeBullets.map((b) => b.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('')}]\\s*`);
|
|
@@ -105,6 +90,36 @@ function rowCells(row) {
|
|
|
105
90
|
// Text nodes have no tagName, hence the optional chain
|
|
106
91
|
return row.childNodes.filter((node) => node.tagName?.toLowerCase() === 'td');
|
|
107
92
|
}
|
|
93
|
+
// Mammoth renders footnotes/endnotes as a trailing <ol> whose items carry ids
|
|
94
|
+
// like `footnote-1` / `endnote-1`.
|
|
95
|
+
const NOTE_ITEM_ID = /^(?:foot|end)note-\d+$/;
|
|
96
|
+
function isNoteList(list) {
|
|
97
|
+
return list.childNodes.some((node) => node.tagName?.toLowerCase() === 'li' &&
|
|
98
|
+
NOTE_ITEM_ID.test(node.getAttribute('id') ?? ''));
|
|
99
|
+
}
|
|
100
|
+
// Link schemes allowed through to the Markdown. Fragments and relative URLs
|
|
101
|
+
// resolve against an https: base below, so they pass too.
|
|
102
|
+
const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
|
|
103
|
+
// Browsers (via the WHATWG URL parser) ignore leading and trailing C0 controls
|
|
104
|
+
// and spaces, so `\x01javascript:` is still a `javascript:` URL. String#trim()
|
|
105
|
+
// doesn't remove them, so strip them explicitly along with other whitespace.
|
|
106
|
+
// eslint-disable-next-line no-control-regex
|
|
107
|
+
const EDGE_CONTROLS_REGEX = /^[\s\u0000-\u001F]+|[\s\u0000-\u001F]+$/g;
|
|
108
|
+
// Returns the cleaned href if it's safe to keep, or null if the link should be
|
|
109
|
+
// unwrapped to its text. Parsing with the WHATWG URL parser (available in Node
|
|
110
|
+
// and browsers) determines the scheme exactly as a browser would, including
|
|
111
|
+
// tabs/newlines inside the scheme (`java\tscript:`) and any letter case.
|
|
112
|
+
function safeHref(href) {
|
|
113
|
+
const trimmed = href.replace(EDGE_CONTROLS_REGEX, '');
|
|
114
|
+
let url;
|
|
115
|
+
try {
|
|
116
|
+
url = new URL(trimmed, 'https://base.invalid/');
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return SAFE_LINK_PROTOCOLS.has(url.protocol) ? trimmed : null;
|
|
122
|
+
}
|
|
108
123
|
// Process HTML in a single pass: optionally strip images, convert table
|
|
109
124
|
// headers, and remove unicode bullets. This is more efficient than parsing the
|
|
110
125
|
// HTML twice.
|
|
@@ -178,64 +193,150 @@ export function processHtml(html, opts = {}) {
|
|
|
178
193
|
listItem.innerHTML = cleanedContent;
|
|
179
194
|
}
|
|
180
195
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
196
|
+
// Optionally render numbered lists as bullet lists by renaming <ol> to <ul>.
|
|
197
|
+
// Doing it on the DOM (rather than rewriting `1.` markers in the Markdown)
|
|
198
|
+
// can't touch numbered lines inside code blocks or literal text. Mammoth's
|
|
199
|
+
// footnote/endnote list stays ordered: it's not a real list, and the footnote
|
|
200
|
+
// conversion (or, with footnotes preserved, the numbered note list) relies on
|
|
201
|
+
// it. Runs after the unicode-bullet strip above so formerly numbered items
|
|
202
|
+
// keep their content as-is.
|
|
203
|
+
if (opts.bulletLists) {
|
|
204
|
+
root.querySelectorAll('ol').forEach((list) => {
|
|
205
|
+
if (isNoteList(list))
|
|
206
|
+
return;
|
|
207
|
+
list.tagName = 'ul';
|
|
208
|
+
list.removeAttribute('start');
|
|
209
|
+
list.removeAttribute('type');
|
|
193
210
|
});
|
|
194
|
-
service.use(turndownPluginGfm.gfm);
|
|
195
|
-
if (keepTags.length > 0) {
|
|
196
|
-
service.keep(keepTags);
|
|
197
|
-
}
|
|
198
|
-
return service;
|
|
199
|
-
}
|
|
200
|
-
if (!turndownServiceInstance) {
|
|
201
|
-
turndownServiceInstance = new TurndownService(defaultTurndownOptions);
|
|
202
|
-
turndownServiceInstance.use(turndownPluginGfm.gfm);
|
|
203
211
|
}
|
|
204
|
-
return
|
|
212
|
+
return root.toString();
|
|
213
|
+
}
|
|
214
|
+
// Mammoth renders footnotes/endnotes as a superscript reference link plus a
|
|
215
|
+
// trailing ordered list of note bodies with `↑` backlinks, not real Markdown
|
|
216
|
+
// footnotes:
|
|
217
|
+
//
|
|
218
|
+
// <p>Text<sup><a href="#footnote-1" id="footnote-ref-1">[1]</a></sup>.</p>
|
|
219
|
+
// <ol><li id="footnote-1"><p>Body. <a href="#footnote-ref-1">↑</a></p></li></ol>
|
|
220
|
+
//
|
|
221
|
+
// These Turndown rules emit GFM/Pandoc footnote syntax instead (`[^1]`
|
|
222
|
+
// references and `[^1]:` definitions), keyed on Mammoth's stable anchor ids.
|
|
223
|
+
// Endnotes use the same numbering scheme, so they share it.
|
|
224
|
+
const NOTE_REF_HREF = /^#(?:foot|end)note-(\d+)$/;
|
|
225
|
+
const NOTE_BACKLINK_HREF = /^#(?:foot|end)note-ref-\d+$/;
|
|
226
|
+
function addFootnoteRules(service) {
|
|
227
|
+
// The reference: a <sup> wrapping only the link to the note.
|
|
228
|
+
service.addRule('footnoteReference', {
|
|
229
|
+
filter: (node) => {
|
|
230
|
+
const link = node.firstElementChild;
|
|
231
|
+
return (node.nodeName === 'SUP' &&
|
|
232
|
+
node.childElementCount === 1 &&
|
|
233
|
+
link?.nodeName === 'A' &&
|
|
234
|
+
NOTE_REF_HREF.test(link.getAttribute('href') ?? ''));
|
|
235
|
+
},
|
|
236
|
+
replacement: (_content, node) => {
|
|
237
|
+
const href = node.firstElementChild?.getAttribute('href') ?? '';
|
|
238
|
+
return `[^${NOTE_REF_HREF.exec(href)?.[1]}]`;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
// The `↑` link back to the reference is meaningless in Markdown footnotes.
|
|
242
|
+
service.addRule('footnoteBacklink', {
|
|
243
|
+
filter: (node) => node.nodeName === 'A' &&
|
|
244
|
+
NOTE_BACKLINK_HREF.test(node.getAttribute('href') ?? ''),
|
|
245
|
+
replacement: () => '',
|
|
246
|
+
});
|
|
247
|
+
// Each note becomes a definition. Later paragraphs of a multi-paragraph note
|
|
248
|
+
// are indented four spaces, which GFM/Pandoc read as a continuation.
|
|
249
|
+
service.addRule('footnoteDefinition', {
|
|
250
|
+
filter: (node) => node.nodeName === 'LI' &&
|
|
251
|
+
NOTE_ITEM_ID.test(node.getAttribute('id') ?? ''),
|
|
252
|
+
replacement: (content, node) => {
|
|
253
|
+
const num = node.getAttribute('id')?.replace(/^\D+/, '');
|
|
254
|
+
const body = content
|
|
255
|
+
.trim()
|
|
256
|
+
.split('\n')
|
|
257
|
+
.map((line, i) => i === 0 || line.trim() === '' ? line : ` ${line}`)
|
|
258
|
+
.join('\n');
|
|
259
|
+
return `[^${num}]: ${body}\n\n`;
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
// Allowlist link schemes on the DOM Turndown converts. A Word hyperlink (or
|
|
264
|
+
// caller-supplied HTML) can target any URL, and Turndown's own `javascript:`
|
|
265
|
+
// check is bypassable (e.g. a leading U+0001), so keep only http(s)/mailto,
|
|
266
|
+
// fragment and relative links; drop the href of anything else so the link
|
|
267
|
+
// renders as its text and can't become a script link downstream.
|
|
268
|
+
//
|
|
269
|
+
// This runs as a rule filter rather than a separate HTML pre-pass so it sees
|
|
270
|
+
// exactly the DOM Turndown builds (no parser differential; e.g. node-html-parser
|
|
271
|
+
// treats <pre> and <noscript> content as raw text). Turndown asks the rules for
|
|
272
|
+
// a node before converting its children, and it's added last so it's checked
|
|
273
|
+
// first, so every <a> is cleaned before any rule reads its href or an ancestor
|
|
274
|
+
// is kept verbatim as HTML (keepTags, GFM tables it can't convert). The filter
|
|
275
|
+
// never matches: it only cleans the node and lets the normal rules convert it.
|
|
276
|
+
function addLinkAllowlistRule(service) {
|
|
277
|
+
service.addRule('linkSchemeAllowlist', {
|
|
278
|
+
filter: (node) => {
|
|
279
|
+
if (node.nodeName !== 'A')
|
|
280
|
+
return false;
|
|
281
|
+
const href = node.getAttribute('href');
|
|
282
|
+
if (href === null)
|
|
283
|
+
return false;
|
|
284
|
+
const safe = safeHref(href);
|
|
285
|
+
if (safe === null)
|
|
286
|
+
node.removeAttribute('href');
|
|
287
|
+
else if (safe !== href)
|
|
288
|
+
node.setAttribute('href', safe);
|
|
289
|
+
return false;
|
|
290
|
+
},
|
|
291
|
+
replacement: (content) => content,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
function createTurndownService(options, keepTags, gfmFootnotes) {
|
|
295
|
+
const service = new TurndownService({
|
|
296
|
+
...defaultTurndownOptions,
|
|
297
|
+
...options,
|
|
298
|
+
});
|
|
299
|
+
service.use(turndownPluginGfm.gfm);
|
|
300
|
+
if (keepTags.length > 0)
|
|
301
|
+
service.keep(keepTags);
|
|
302
|
+
if (gfmFootnotes)
|
|
303
|
+
addFootnoteRules(service);
|
|
304
|
+
addLinkAllowlistRule(service);
|
|
305
|
+
return service;
|
|
306
|
+
}
|
|
307
|
+
// Reusable services for the default options, keyed by keep-tags (e.g. '' or
|
|
308
|
+
// 'u' for --underline) and footnote mode, so repeat conversions don't rebuild
|
|
309
|
+
// Turndown and its GFM rules. `keep()` and `addRule()` mutate a service, hence
|
|
310
|
+
// one per combination.
|
|
311
|
+
const turndownServices = new Map();
|
|
312
|
+
function getTurndownService(options = {}, keepTags = [], gfmFootnotes = false) {
|
|
313
|
+
// Caller-supplied Turndown options may hold functions (custom rules), which
|
|
314
|
+
// can't be keyed reliably; build a fresh service for those.
|
|
315
|
+
if (Object.keys(options).length > 0) {
|
|
316
|
+
return createTurndownService(options, keepTags, gfmFootnotes);
|
|
317
|
+
}
|
|
318
|
+
const key = `${[...keepTags].sort().join(',')}|${gfmFootnotes}`;
|
|
319
|
+
let service = turndownServices.get(key);
|
|
320
|
+
if (!service) {
|
|
321
|
+
service = createTurndownService({}, keepTags, gfmFootnotes);
|
|
322
|
+
turndownServices.set(key, service);
|
|
323
|
+
}
|
|
324
|
+
return service;
|
|
205
325
|
}
|
|
206
326
|
// Convert HTML to GitHub-flavored Markdown. `keepTags` lists HTML tags to
|
|
207
327
|
// preserve verbatim as inline HTML (e.g. `['u']` to keep underlines) rather
|
|
208
|
-
// than let Turndown strip them to plain text.
|
|
209
|
-
|
|
328
|
+
// than let Turndown strip them to plain text. `gfmFootnotes` rewrites Mammoth's
|
|
329
|
+
// footnote/endnote markup into `[^1]` footnotes (see addFootnoteRules).
|
|
330
|
+
export function htmlToMd(html, options = {}, keepTags = [], gfmFootnotes = false) {
|
|
210
331
|
// Turndown's DOM parser decodes entities exactly once. Don't pre-decode:
|
|
211
332
|
// that would turn literal text like `<b>` into markup and truncate
|
|
212
333
|
// attribute values containing `"`.
|
|
213
|
-
const turndownService = getTurndownService(options, keepTags);
|
|
334
|
+
const turndownService = getTurndownService(options, keepTags, gfmFootnotes);
|
|
214
335
|
return turndownService.turndown(html).trim();
|
|
215
336
|
}
|
|
216
337
|
// Pre-compiled regex patterns for better performance
|
|
217
|
-
const numberedListRegex = /^(\s*)(\d+)\.\s/;
|
|
218
|
-
const fenceRegex = /^\s*(`{3,}|~{3,})/;
|
|
219
338
|
const nonBreakingSpacesRegex = /[\u00A0\u2007\u202F\u2060\uFEFF]/g;
|
|
220
339
|
const smartQuotesRegex = /[\u201C\u201D\u2018\u2019]/g;
|
|
221
|
-
// Mammoth renders footnotes/endnotes as a superscript reference link plus a
|
|
222
|
-
// trailing ordered list of note bodies with `\u2191` backlinks \u2014 not real Markdown
|
|
223
|
-
// footnotes. These two regexes rewrite that into GFM/Pandoc footnote syntax.
|
|
224
|
-
// Both anchor on Mammoth's stable anchor ids (`#footnote-N` / `#footnote-ref-N`,
|
|
225
|
-
// or the `endnote` variants), never the escaped display label, which prettier
|
|
226
|
-
// and markdownlint may re-escape.
|
|
227
|
-
//
|
|
228
|
-
// Reference in the body, e.g. `<sup>[\[1\]](#footnote-1)</sup>` \u2192 `[^1]`. The
|
|
229
|
-
// non-greedy link text backtracks past the escaped `\]` inside the label, but
|
|
230
|
-
// can't cross a `<`, so an earlier, unrelated `<sup>` link isn't swallowed.
|
|
231
|
-
const footnoteRefRegex = /<sup>\[[^<]*?\]\(#(?:foot|end)note-(\d+)\)<\/sup>/g;
|
|
232
|
-
// Definition list item, e.g. `1. Body text. [\u2191](#footnote-ref-1)` \u2192
|
|
233
|
-
// `[^1]: Body text.`. The list marker is unreliable (prettier renumbers), so the
|
|
234
|
-
// footnote number comes from the backlink. Only single-line note bodies match:
|
|
235
|
-
// a multi-paragraph body (a rare Word construct) puts the backlink on an indented
|
|
236
|
-
// continuation line the (newline-free) body group can't reach, so it's left as-is
|
|
237
|
-
// \u2014 see convertFootnotes for how the matching reference is then also left raw.
|
|
238
|
-
const footnoteDefRegex = /^[ \t]*\d+\.[ \t]+(.*?)[ \t]*\[\u2191\]\(#(?:foot|end)note-ref-(\d+)\)[ \t]*$/gm;
|
|
239
340
|
// Map for non-breaking space replacements
|
|
240
341
|
const nonBreakingSpaceMap = {
|
|
241
342
|
'\u00A0': ' ', // Non-breaking space
|
|
@@ -251,50 +352,12 @@ const smartQuoteMap = {
|
|
|
251
352
|
'\u2018': "'", // Left single quotation mark
|
|
252
353
|
'\u2019': "'", // Right single quotation mark
|
|
253
354
|
};
|
|
254
|
-
// Convert numbered lists to bullet lists, leaving fenced code blocks untouched
|
|
255
|
-
function convertNumberedListsToBullets(md) {
|
|
256
|
-
let fence = null;
|
|
257
|
-
return md
|
|
258
|
-
.split('\n')
|
|
259
|
-
.map((line) => {
|
|
260
|
-
const marker = line.match(fenceRegex)?.[1];
|
|
261
|
-
if (marker) {
|
|
262
|
-
// A fence closes only with the same character, at least as long
|
|
263
|
-
if (fence === null)
|
|
264
|
-
fence = marker;
|
|
265
|
-
else if (marker[0] === fence[0] && marker.length >= fence.length)
|
|
266
|
-
fence = null;
|
|
267
|
-
return line;
|
|
268
|
-
}
|
|
269
|
-
return fence === null ? line.replace(numberedListRegex, '$1- ') : line;
|
|
270
|
-
})
|
|
271
|
-
.join('\n');
|
|
272
|
-
}
|
|
273
355
|
// Remove unicode non-breaking spaces and convert smart quotes to ASCII in a single pass
|
|
274
356
|
function normalizeText(md) {
|
|
275
357
|
return md
|
|
276
358
|
.replace(nonBreakingSpacesRegex, (char) => nonBreakingSpaceMap[char])
|
|
277
359
|
.replace(smartQuotesRegex, (char) => smartQuoteMap[char]);
|
|
278
360
|
}
|
|
279
|
-
// Rewrite Mammoth's footnote/endnote markup into GFM/Pandoc footnote syntax.
|
|
280
|
-
// See footnoteRefRegex / footnoteDefRegex for the shapes matched. Runs after
|
|
281
|
-
// lint() and before prettify() so prettier normalizes the resulting footnote
|
|
282
|
-
// block (prettier's markdown parser preserves `[^1]` / `[^1]:`).
|
|
283
|
-
function convertFootnotes(md) {
|
|
284
|
-
// Rewrite the note definitions first, recording which footnote numbers were
|
|
285
|
-
// actually converted. A number won't convert if its body spans multiple
|
|
286
|
-
// paragraphs (footnoteDefRegex only matches single-line bodies).
|
|
287
|
-
const converted = new Set();
|
|
288
|
-
const withDefs = md.replace(footnoteDefRegex, (_match, body, num) => {
|
|
289
|
-
converted.add(num);
|
|
290
|
-
return `[^${num}]: ${body}`;
|
|
291
|
-
});
|
|
292
|
-
// Only convert references whose definition converted. Rewriting a reference
|
|
293
|
-
// whose definition was left as a raw list item would produce a dangling `[^N]`
|
|
294
|
-
// with no target; gating on `converted` keeps that (rare) note as intact raw
|
|
295
|
-
// `<sup>` + list markup instead.
|
|
296
|
-
return withDefs.replace(footnoteRefRegex, (match, num) => converted.has(num) ? `[^${num}]` : match);
|
|
297
|
-
}
|
|
298
361
|
// Lint the Markdown and correct any issues
|
|
299
362
|
function lint(md) {
|
|
300
363
|
const lintResult = markdownlint.lint({ strings: { md } });
|
|
@@ -309,20 +372,22 @@ async function prettify(md) {
|
|
|
309
372
|
return formatted.trim();
|
|
310
373
|
}
|
|
311
374
|
// Extract document properties from a .docx file
|
|
375
|
+
// The text of docProps/core.xml's classification fields (cp:keywords,
|
|
376
|
+
// cp:category, cp:contentStatus), with or without a namespace prefix.
|
|
377
|
+
const CORE_CLASSIFICATION_FIELDS = /<(?:\w+:)?(?:keywords|category|contentStatus)\b[^>]*>([^<]*)</g;
|
|
312
378
|
export async function extractDocumentProperties(input) {
|
|
313
379
|
const properties = {};
|
|
380
|
+
// A path that can't be read is the caller's problem, not a "no properties"
|
|
381
|
+
// result, so surface it as a typed error before the lenient parsing below.
|
|
382
|
+
let arrayBuffer;
|
|
383
|
+
try {
|
|
384
|
+
arrayBuffer =
|
|
385
|
+
typeof input === 'string' ? await readFileBytes(input) : input;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
classifyConversionError(error, typeof input === 'string' ? input : undefined);
|
|
389
|
+
}
|
|
314
390
|
try {
|
|
315
|
-
let arrayBuffer;
|
|
316
|
-
if (typeof input === 'string') {
|
|
317
|
-
// Validate the file path to prevent path traversal attacks
|
|
318
|
-
const safePath = validateFilePath(input);
|
|
319
|
-
// Read file from path and convert to ArrayBuffer
|
|
320
|
-
const fileBuffer = await fs.readFile(safePath);
|
|
321
|
-
arrayBuffer = toArrayBuffer(fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength));
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
arrayBuffer = input;
|
|
325
|
-
}
|
|
326
391
|
const zip = await JSZip.loadAsync(arrayBuffer);
|
|
327
392
|
// Check for encryption - encrypted files have EncryptionInfo and EncryptedPackage
|
|
328
393
|
const encryptionInfo = zip.file('EncryptionInfo');
|
|
@@ -333,10 +398,14 @@ export async function extractDocumentProperties(input) {
|
|
|
333
398
|
const corePropsFile = zip.file('docProps/core.xml');
|
|
334
399
|
if (corePropsFile) {
|
|
335
400
|
const coreXml = await corePropsFile.async('string');
|
|
336
|
-
// Look for
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
401
|
+
// Look for confidentiality markers in the classification fields only.
|
|
402
|
+
// Free-text fields (title, subject, description, author) are skipped, so a
|
|
403
|
+
// title like "Case-sensitive search" isn't flagged.
|
|
404
|
+
for (const [, value] of coreXml.matchAll(CORE_CLASSIFICATION_FIELDS)) {
|
|
405
|
+
if (/confidential|sensitive/i.test(value)) {
|
|
406
|
+
properties.confidentiality = 'detected in core properties';
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
340
409
|
}
|
|
341
410
|
}
|
|
342
411
|
// Try to read custom properties
|
|
@@ -417,9 +486,7 @@ async function loadInput(input) {
|
|
|
417
486
|
let bytes;
|
|
418
487
|
if (typeof input === 'string') {
|
|
419
488
|
validateFileExtension(input);
|
|
420
|
-
|
|
421
|
-
const fileBuffer = await fs.readFile(validateFilePath(input));
|
|
422
|
-
bytes = toArrayBuffer(fileBuffer.buffer.slice(fileBuffer.byteOffset, fileBuffer.byteOffset + fileBuffer.byteLength));
|
|
489
|
+
bytes = await readFileBytes(input);
|
|
423
490
|
}
|
|
424
491
|
else {
|
|
425
492
|
bytes = input;
|
|
@@ -432,16 +499,6 @@ async function loadInput(input) {
|
|
|
432
499
|
: { arrayBuffer: bytes };
|
|
433
500
|
return { bytes, mammothInput };
|
|
434
501
|
}
|
|
435
|
-
// Ensure we have an ArrayBuffer (not a SharedArrayBuffer) by copying if needed
|
|
436
|
-
function toArrayBuffer(buffer) {
|
|
437
|
-
if (buffer instanceof ArrayBuffer) {
|
|
438
|
-
return buffer;
|
|
439
|
-
}
|
|
440
|
-
const uint8Array = new Uint8Array(buffer);
|
|
441
|
-
const newArrayBuffer = new ArrayBuffer(uint8Array.byteLength);
|
|
442
|
-
new Uint8Array(newArrayBuffer).set(uint8Array);
|
|
443
|
-
return newArrayBuffer;
|
|
444
|
-
}
|
|
445
502
|
// Synthetic paragraph-style name applied to code paragraphs detected by font
|
|
446
503
|
// (see tagCodeParagraphs). It's mapped to a fenced code block alongside the real
|
|
447
504
|
// Word/LibreOffice preformatted styles below.
|
|
@@ -510,11 +567,21 @@ function isMonospaceParagraph(paragraph) {
|
|
|
510
567
|
// fenced code blocks. Word often encodes code as monospace runs on Normal
|
|
511
568
|
// paragraphs (no code paragraph style), and Mammoth discards font information
|
|
512
569
|
// once it emits HTML — the document model is the only place the signal survives.
|
|
570
|
+
// Headings, titles, and list items keep their structure even when set wholly in
|
|
571
|
+
// a monospace font: their style (or numbering) is a stronger signal than font.
|
|
572
|
+
const STRUCTURAL_STYLE_RE = /^(heading|title|subtitle)\b/i;
|
|
573
|
+
function isStructuralParagraph(paragraph) {
|
|
574
|
+
const styleName = paragraph.styleName;
|
|
575
|
+
return (Boolean(paragraph.numbering) ||
|
|
576
|
+
(typeof styleName === 'string' && STRUCTURAL_STYLE_RE.test(styleName)));
|
|
577
|
+
}
|
|
513
578
|
function tagCodeParagraphs(node) {
|
|
514
579
|
const transformed = node.children
|
|
515
580
|
? { ...node, children: node.children.map(tagCodeParagraphs) }
|
|
516
581
|
: node;
|
|
517
|
-
if (transformed.type === 'paragraph' &&
|
|
582
|
+
if (transformed.type === 'paragraph' &&
|
|
583
|
+
!isStructuralParagraph(transformed) &&
|
|
584
|
+
isMonospaceParagraph(transformed)) {
|
|
518
585
|
return {
|
|
519
586
|
...transformed,
|
|
520
587
|
styleId: 'W2MCodeBlock',
|
|
@@ -550,6 +617,7 @@ const CONTENT_TYPE_EXTENSIONS = {
|
|
|
550
617
|
// Pick a file extension for an extracted image. Falls back to the content
|
|
551
618
|
// type's subtype when it's a clean alphanumeric token, else `bin`. Exported for
|
|
552
619
|
// unit testing since only a PNG fixture exists.
|
|
620
|
+
/** @internal */
|
|
553
621
|
export function extensionForContentType(contentType) {
|
|
554
622
|
const normalized = contentType.toLowerCase();
|
|
555
623
|
const known = CONTENT_TYPE_EXTENSIONS[normalized];
|
|
@@ -558,17 +626,19 @@ export function extensionForContentType(contentType) {
|
|
|
558
626
|
const subtype = normalized.split('/')[1] ?? '';
|
|
559
627
|
return /^[a-z0-9]+$/.test(subtype) ? subtype : 'bin';
|
|
560
628
|
}
|
|
561
|
-
//
|
|
562
|
-
//
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
629
|
+
// Read an image's raw bytes as a standalone Uint8Array. Mammoth types
|
|
630
|
+
// `readAsArrayBuffer()` as an ArrayBuffer, but at runtime it returns JSZip's
|
|
631
|
+
// `uint8array` output, which can be a Node Buffer or a view into a larger buffer
|
|
632
|
+
// (e.g. an uncompressed entry sliced from the .docx itself). `new Uint8Array()`
|
|
633
|
+
// covers every case: it wraps a real ArrayBuffer whole and copies any typed-array
|
|
634
|
+
// view into a fresh, exactly-sized buffer. That matters for the worker, which
|
|
635
|
+
// transfers each image's `.buffer` to the page (src/converter.worker.ts):
|
|
636
|
+
// transferring a shared view would detach unrelated bytes.
|
|
637
|
+
async function readImageBytes(image) {
|
|
638
|
+
// No instanceof check: under Jest's vm modules (and across worker realms) a
|
|
639
|
+
// Uint8Array may not be an instance of this realm's class, and the
|
|
640
|
+
// constructor handles both shapes anyway.
|
|
641
|
+
return new Uint8Array(await image.readAsArrayBuffer());
|
|
572
642
|
}
|
|
573
643
|
// Build a Mammoth `convertImage` handler that pulls each image out into a byte
|
|
574
644
|
// array with a deterministic relative path (`imageDir/imageN.ext`) instead of
|
|
@@ -577,7 +647,7 @@ function base64ToBytes(b64) {
|
|
|
577
647
|
function createImageExtractor(imageDir) {
|
|
578
648
|
const images = [];
|
|
579
649
|
const convertImage = mammoth.images.imgElement(async (image) => {
|
|
580
|
-
const bytes =
|
|
650
|
+
const bytes = await readImageBytes(image);
|
|
581
651
|
const ext = extensionForContentType(image.contentType);
|
|
582
652
|
const path = `${imageDir}/image${images.length + 1}.${ext}`;
|
|
583
653
|
images.push({ path, contentType: image.contentType, bytes });
|
|
@@ -609,21 +679,16 @@ async function runConversionPipeline(mammothInput, options) {
|
|
|
609
679
|
};
|
|
610
680
|
}
|
|
611
681
|
const mammothResult = await mammoth.convertToHtml(mammothInput, mammothOptions);
|
|
682
|
+
// Numbered lists stay numbered by default; flatten to bullets on request.
|
|
612
683
|
const processedHtml = processHtml(mammothResult.value, {
|
|
613
684
|
stripImages: options.images === 'strip',
|
|
685
|
+
bulletLists: options.numberedLists === 'bullets',
|
|
614
686
|
});
|
|
615
|
-
|
|
687
|
+
// Footnotes become GFM `[^1]` by default; keep Mammoth's markup on request.
|
|
688
|
+
const md = htmlToMd(processedHtml, options.turndown, preserveUnderline ? ['u'] : [], options.footnotes !== 'preserve');
|
|
616
689
|
const normalizedMd = normalizeText(md);
|
|
617
690
|
const cleanedMd = lint(normalizedMd);
|
|
618
|
-
|
|
619
|
-
const footnotedMd = options.footnotes === 'preserve' ? cleanedMd : convertFootnotes(cleanedMd);
|
|
620
|
-
// Numbered lists stay numbered by default; flatten to bullets on request.
|
|
621
|
-
// This must run after convertFootnotes, whose definition regex matches the
|
|
622
|
-
// numbered `1. body [\u2191](#footnote-ref-1)` list items.
|
|
623
|
-
const listMd = options.numberedLists === 'bullets'
|
|
624
|
-
? convertNumberedListsToBullets(footnotedMd)
|
|
625
|
-
: footnotedMd;
|
|
626
|
-
const formattedMd = await prettify(listMd);
|
|
691
|
+
const formattedMd = await prettify(cleanedMd);
|
|
627
692
|
return {
|
|
628
693
|
markdown: formattedMd,
|
|
629
694
|
messages: mammothResult.messages,
|
|
@@ -682,7 +747,7 @@ function classifyConversionError(error, filePath) {
|
|
|
682
747
|
if (isInvalidDocxError(errorMessage)) {
|
|
683
748
|
// Note: For ArrayBuffer inputs (e.g., web uploads), filePath will be
|
|
684
749
|
// undefined, so the message won't include the original filename.
|
|
685
|
-
throw new InvalidFileError(filePath);
|
|
750
|
+
throw new InvalidFileError(filePath, error);
|
|
686
751
|
}
|
|
687
752
|
// Wrap other errors with a general conversion error
|
|
688
753
|
throw new ConversionError('An error occurred while converting the document. Please ensure the file is a valid .docx file and try again.', error instanceof Error ? error : undefined);
|
package/build/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,iBAAiB,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAoB,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AACrC,OAAO,KAAK,gBAAgB,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,aAAa,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AA2ExB,8EAA8E;AAC9E,2CAA2C;AAC3C,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD,kDAAkD;AAClD,MAAM,OAAO,oBAAqB,SAAQ,mBAAmB;IAC3D,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,wCAAwC;AACxC,MAAM,OAAO,iBAAkB,SAAQ,mBAAmB;IACxD,YAAY,QAAiB;QAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,KAAK,CACH,iBAAiB,QAAQ,8DAA8D,CACxF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF;AAED,iDAAiD;AACjD,MAAM,OAAO,gBAAiB,SAAQ,mBAAmB;IACvD,YAAY,QAAiB;QAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,KAAK,CACH,eAAe,QAAQ,iIAAiI,CACzJ,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,2CAA2C;AAC3C,MAAM,OAAO,mBAAoB,SAAQ,mBAAmB;IAC1D,YAAY,QAAiB;QAC3B,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QACnD,KAAK,CACH,oBAAoB,QAAQ,wDAAwD,CACrF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,mDAAmD;AACnD,MAAM,OAAO,eAAgB,SAAQ,mBAAmB;IACtD,YAAY,OAAe,EAAE,aAAqB;QAChD,4DAA4D;QAC5D,KAAK,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACrE,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAQD,MAAM,sBAAsB,GAAoB;IAC9C,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,QAAQ;IACxB,gBAAgB,EAAE,GAAG;CACtB,CAAC;AAEF,iEAAiE;AACjE,MAAM,UAAU,qBAAqB,CAAC,QAAgB;IACpD,mEAAmE;IACnE,MAAM,QAAQ,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACxC,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAExE,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,oBAAoB,CAC5B,+GAA+G,CAChH,CAAC;IACJ,CAAC;AACH,CAAC;AAED,yFAAyF;AACzF,SAAS,gBAAgB,CAAC,QAAgB;IACxC,sEAAsE;IACtE,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,8CAA8C;IAC9C,6EAA6E;IAC7E,0DAA0D;IAC1D,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE5C,+EAA+E;IAC/E,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACxE,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;QAC3C,IAAI,YAAY,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,MAAM,qBAAqB,GAAG,CAAC,eAAe,EAAE,qBAAqB,CAAC,CAAC;IACvE,KAAK,MAAM,aAAa,IAAI,qBAAqB,EAAE,CAAC;QAClD,IAAI,YAAY,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;YACvE,MAAM,IAAI,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,qDAAqD;AACrD,oDAAoD;AACpD,uDAAuD;AACvD,4CAA4C;AAC5C,6EAA6E;AAC7E,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAChE,MAAM,WAAW,GAAG,IAAI,MAAM,CAC5B,SAAS,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,CAC7F,CAAC;AAEF,mEAAmE;AACnE,SAAS,QAAQ,CAAC,GAAgB;IAChC,uDAAuD;IACvD,OAAQ,GAAG,CAAC,UAA4B,CAAC,MAAM,CAC7C,CAAC,IAAiB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,CAC5D,CAAC;AACJ,CAAC;AAED,wEAAwE;AACxE,+EAA+E;AAC/E,cAAc;AACd,yCAAyC;AACzC,MAAM,UAAU,WAAW,CACzB,IAAY,EACZ,OAAkC,EAAE;IAEpC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IAEzB,4EAA4E;IAC5E,mDAAmD;IACnD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,GAAgB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,+EAA+E;IAC/E,iFAAiF;IACjF,gFAAgF;IAChF,8EAA8E;IAC9E,yDAAyD;IACzD,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAkB,EAAE,EAAE;QAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC7C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC1C,wEAAwE;QACxE,4EAA4E;QAC5E,uDAAuD;QACvD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC9B,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE;YAAE,OAAO;QACnE,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,sDAAsD;IACtD,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAkB,EAAE,EAAE;QAC5D,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ;YAAE,OAAO;QAEtB,uDAAuD;QACvD,IAAI,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC;YAAE,OAAO;QAEzC,0EAA0E;QAC1E,sDAAsD;QACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,OAAO,GACX,KAAK,CAAC,MAAM,KAAK,CAAC;YAClB,KAAK,CAAC,KAAK,CACT,CAAC,IAAiB,EAAE,EAAE,CACpB,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAC1D,CAAC;QAEJ,IAAI,OAAO,EAAE,CAAC;YACZ,qEAAqE;YACrE,QAAQ,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,OAAO,EAAE,CAAC;gBACZ,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,IAAiB,EAAE,EAAE;oBAC9C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;gBACtB,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;aAAM,CAAC;YACN,sCAAsC;YACtC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAiB,EAAE,EAAE;gBAClC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACtB,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,oEAAoE;IACpE,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,QAAqB,EAAE,EAAE;QAC/D,qEAAqE;QACrE,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC;QACvC,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC5D,IAAI,cAAc,KAAK,WAAW,EAAE,CAAC;YACnC,QAAQ,CAAC,SAAS,GAAG,cAAc,CAAC;QACtC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;AACzB,CAAC;AAED,+EAA+E;AAC/E,IAAI,uBAAuB,GAA2B,IAAI,CAAC;AAE3D,SAAS,kBAAkB,CACzB,UAAkB,EAAE,EACpB,WAAqB,EAAE;IAEvB,8EAA8E;IAC9E,2EAA2E;IAC3E,2EAA2E;IAC3E,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3D,MAAM,OAAO,GAAG,IAAI,eAAe,CAAC;YAClC,GAAG,sBAAsB;YACzB,GAAG,OAAO;SACX,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACzB,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,CAAC,uBAAuB,EAAE,CAAC;QAC7B,uBAAuB,GAAG,IAAI,eAAe,CAAC,sBAAsB,CAAC,CAAC;QACtE,uBAAuB,CAAC,GAAG,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,uBAAuB,CAAC;AACjC,CAAC;AAED,0EAA0E;AAC1E,4EAA4E;AAC5E,8CAA8C;AAC9C,MAAM,UAAU,QAAQ,CACtB,IAAY,EACZ,UAAkB,EAAE,EACpB,WAAqB,EAAE;IAEvB,yEAAyE;IACzE,2EAA2E;IAC3E,wCAAwC;IACxC,MAAM,eAAe,GAAG,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC9D,OAAO,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC/C,CAAC;AAED,qDAAqD;AACrD,MAAM,iBAAiB,GAAG,iBAAiB,CAAC;AAC5C,MAAM,UAAU,GAAG,mBAAmB,CAAC;AACvC,MAAM,sBAAsB,GAAG,mCAAmC,CAAC;AACnE,MAAM,gBAAgB,GAAG,6BAA6B,CAAC;AAEvD,4EAA4E;AAC5E,wFAAwF;AACxF,6EAA6E;AAC7E,iFAAiF;AACjF,8EAA8E;AAC9E,kCAAkC;AAClC,EAAE;AACF,mFAAmF;AACnF,8EAA8E;AAC9E,4EAA4E;AAC5E,MAAM,gBAAgB,GAAG,oDAAoD,CAAC;AAC9E,8EAA8E;AAC9E,iFAAiF;AACjF,+EAA+E;AAC/E,kFAAkF;AAClF,kFAAkF;AAClF,oFAAoF;AACpF,MAAM,gBAAgB,GACpB,iFAAiF,CAAC;AAEpF,0CAA0C;AAC1C,MAAM,mBAAmB,GAA8B;IACrD,QAAQ,EAAE,GAAG,EAAE,qBAAqB;IACpC,QAAQ,EAAE,GAAG,EAAE,eAAe;IAC9B,QAAQ,EAAE,GAAG,EAAE,wBAAwB;IACvC,QAAQ,EAAE,EAAE,EAAE,8CAA8C;IAC5D,QAAQ,EAAE,EAAE,EAAE,kCAAkC;CACjD,CAAC;AAEF,mCAAmC;AACnC,MAAM,aAAa,GAA8B;IAC/C,QAAQ,EAAE,GAAG,EAAE,6BAA6B;IAC5C,QAAQ,EAAE,GAAG,EAAE,8BAA8B;IAC7C,QAAQ,EAAE,GAAG,EAAE,6BAA6B;IAC5C,QAAQ,EAAE,GAAG,EAAE,8BAA8B;CAC9C,CAAC;AAEF,+EAA+E;AAC/E,SAAS,6BAA6B,CAAC,EAAU;IAC/C,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,OAAO,EAAE;SACN,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3C,IAAI,MAAM,EAAE,CAAC;YACX,gEAAgE;YAChE,IAAI,KAAK,KAAK,IAAI;gBAAE,KAAK,GAAG,MAAM,CAAC;iBAC9B,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM;gBAC9D,KAAK,GAAG,IAAI,CAAC;YACf,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACzE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,wFAAwF;AACxF,SAAS,aAAa,CAAC,EAAU;IAC/B,OAAO,EAAE;SACN,OAAO,CAAC,sBAAsB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC;SACpE,OAAO,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,6EAA6E;AAC7E,iEAAiE;AACjE,SAAS,gBAAgB,CAAC,EAAU;IAClC,4EAA4E;IAC5E,wEAAwE;IACxE,iEAAiE;IACjE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,MAAM,QAAQ,GAAG,EAAE,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QAClE,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,OAAO,KAAK,GAAG,MAAM,IAAI,EAAE,CAAC;IAC9B,CAAC,CAAC,CAAC;IACH,4EAA4E;IAC5E,+EAA+E;IAC/E,6EAA6E;IAC7E,iCAAiC;IACjC,OAAO,QAAQ,CAAC,OAAO,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CACvD,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CACzC,CAAC;AACJ,CAAC;AAED,2CAA2C;AAC3C,SAAS,IAAI,CAAC,EAAU;IACtB,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1D,OAAO,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACjD,CAAC;AAED,oCAAoC;AACpC,KAAK,UAAU,QAAQ,CAAC,EAAU;IAChC,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,EAAE;QAC1C,MAAM,EAAE,UAAU;QAClB,OAAO,EAAE,CAAC,gBAAgB,CAAC;KAC5B,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC;AAC1B,CAAC;AAED,gDAAgD;AAChD,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,KAA2B;IAE3B,MAAM,UAAU,GAAuB,EAAE,CAAC;IAE1C,IAAI,CAAC;QACH,IAAI,WAAwB,CAAC;QAC7B,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,2DAA2D;YAC3D,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;YAEzC,iDAAiD;YACjD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC/C,WAAW,GAAG,aAAa,CACzB,UAAU,CAAC,MAAM,CAAC,KAAK,CACrB,UAAU,CAAC,UAAU,EACrB,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,CAC9C,CACF,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,WAAW,GAAG,KAAK,CAAC;QACtB,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QAE/C,kFAAkF;QAClF,MAAM,cAAc,GAAG,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAClD,IAAI,cAAc,EAAE,CAAC;YACnB,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC;QAC/B,CAAC;QAED,8BAA8B;QAC9B,MAAM,aAAa,GAAG,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QACpD,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACpD,oEAAoE;YACpE,IACE,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAC9C,OAAO,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,EAC3C,CAAC;gBACD,UAAU,CAAC,eAAe,GAAG,6BAA6B,CAAC;YAC7D,CAAC;QACH,CAAC;QAED,gCAAgC;QAChC,MAAM,eAAe,GAAG,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;QACxD,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,SAAS,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACxD,MAAM,cAAc,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;YAE/C,sEAAsE;YACtE,2EAA2E;YAC3E,wFAAwF;YAExF,qEAAqE;YACrE,6CAA6C;YAC7C,6DAA6D;YAC7D,gEAAgE;YAChE,yFAAyF;YACzF,sEAAsE;YACtE,MAAM,kBAAkB,GACtB,sGAAsG,CAAC;YAEzG,MAAM,sBAAsB,GAAG,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClE,MAAM,mBAAmB,GAAG,cAAc,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;YACpE,MAAM,YAAY,GAAG,cAAc,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAE3D,IAAI,sBAAsB,IAAI,YAAY,EAAE,CAAC;gBAC3C,UAAU,CAAC,WAAW,GAAG,+BAA+B,CAAC;YAC3D,CAAC;YAED,IAAI,mBAAmB,EAAE,CAAC;gBACxB,UAAU,CAAC,eAAe,GAAG,+BAA+B,CAAC;YAC/D,CAAC;QACH,CAAC;QAED,gCAAgC;QAChC,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QACnD,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,WAAW,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACvD,IACE,WAAW,CAAC,QAAQ,CAAC,uBAAuB,CAAC;gBAC7C,WAAW,CAAC,QAAQ,CAAC,oBAAoB,CAAC,EAC1C,CAAC;gBACD,UAAU,CAAC,UAAU,GAAG,IAAI,CAAC;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,6DAA6D;QAC7D,2EAA2E;QAC3E,qEAAqE;QACrE,0EAA0E;QAC1E,IACE,OAAO,OAAO,KAAK,WAAW;YAC9B,OAAO,CAAC,GAAG,EAAE,QAAQ,KAAK,aAAa,EACvC,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,gBAAgB,CAAC,UAA8B;IAC7D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CACX,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;QAC3B,QAAQ,CAAC,IAAI,CACX,kDAAkD,UAAU,CAAC,WAAW,yEAAyE,CAClJ,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,eAAe,EAAE,CAAC;QAC/B,QAAQ,CAAC,IAAI,CACX,4DAA4D,UAAU,CAAC,eAAe,iDAAiD,CACxI,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,CAAC,UAAU,EAAE,CAAC;QAC1B,QAAQ,CAAC,IAAI,CACX,iGAAiG,CAClG,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAKD,2EAA2E;AAC3E,6EAA6E;AAC7E,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEvE,SAAS,cAAc,CAAC,KAAkB;IACxC,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,CACL,IAAI,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM;QACpC,aAAa,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CACnD,CAAC;AACJ,CAAC;AAOD,4EAA4E;AAC5E,kFAAkF;AAClF,KAAK,UAAU,SAAS,CAAC,KAA2B;IAClD,IAAI,KAAkB,CAAC;IACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAC7B,2DAA2D;QAC3D,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,KAAK,GAAG,aAAa,CACnB,UAAU,CAAC,MAAM,CAAC,KAAK,CACrB,UAAU,CAAC,UAAU,EACrB,UAAU,CAAC,UAAU,GAAG,UAAU,CAAC,UAAU,CAC9C,CACF,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,GAAG,KAAK,CAAC;IAChB,CAAC;IAED,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,oBAAoB,CAC5B,gIAAgI,CACjI,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAChB,OAAO,MAAM,KAAK,WAAW;QAC3B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;QAChC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAC7B,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;AACjC,CAAC;AAQD,+EAA+E;AAC/E,SAAS,aAAa,CAAC,MAAuB;IAC5C,IAAI,MAAM,YAAY,WAAW,EAAE,CAAC;QAClC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,cAAc,GAAG,IAAI,WAAW,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAC9D,IAAI,UAAU,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IAC/C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,6EAA6E;AAC7E,iFAAiF;AACjF,8CAA8C;AAC9C,MAAM,wBAAwB,GAAG,gBAAgB,CAAC;AAElD,iFAAiF;AACjF,wBAAwB;AACxB,+EAA+E;AAC/E,yDAAyD;AACzD,gFAAgF;AAChF,8EAA8E;AAC9E,wBAAwB;AACxB,gFAAgF;AAChF,8EAA8E;AAC9E,gFAAgF;AAChF,kFAAkF;AAClF,+EAA+E;AAC/E,+CAA+C;AAC/C,MAAM,oBAAoB,GAAG;IAC3B,iBAAiB,wBAAwB,mCAAmC;IAC5E,kEAAkE;IAClE,kEAAkE;CACnE,CAAC;AAEF,6EAA6E;AAC7E,gFAAgF;AAChF,0EAA0E;AAC1E,0EAA0E;AAC1E,iDAAiD;AACjD,SAAS,YAAY,CACnB,cAA0C,EAC1C,EAAE,iBAAiB,EAAkC;IAErD,MAAM,QAAQ,GAAG,cAAc,EAAE,QAAQ,IAAI,EAAE,CAAC;IAChD,MAAM,SAAS,GAAG,CAAC,GAAG,oBAAoB,CAAC,CAAC;IAC5C,IAAI,iBAAiB;QAAE,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChD,OAAO,EAAE,GAAG,cAAc,EAAE,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,GAAG,SAAS,CAAC,EAAE,CAAC;AACtE,CAAC;AAcD,gFAAgF;AAChF,4EAA4E;AAC5E,yEAAyE;AACzE,iEAAiE;AACjE,MAAM,iBAAiB,GACrB,sEAAsE,CAAC;AAEzE,SAAS,eAAe,CAAC,IAA+B;IACtD,OAAO,CAAC,CAAC,IAAI,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,8DAA8D;AAC9D,SAAS,WAAW,CAAC,IAAoB;IACvC,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;IAClD,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAClB,IAAoB,EACpB,MAAwB,EAAE;IAE1B,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK;QAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IAClE,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yEAAyE;AACzE,+EAA+E;AAC/E,iFAAiF;AACjF,iEAAiE;AACjE,SAAS,oBAAoB,CAAC,SAAyB;IACrD,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,CACxC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAC5C,CAAC;IACF,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,8EAA8E;AAC9E,iFAAiF;AACjF,SAAS,iBAAiB,CAAC,IAAoB;IAC7C,MAAM,WAAW,GAAmB,IAAI,CAAC,QAAQ;QAC/C,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE;QAC7D,CAAC,CAAC,IAAI,CAAC;IACT,IAAI,WAAW,CAAC,IAAI,KAAK,WAAW,IAAI,oBAAoB,CAAC,WAAW,CAAC,EAAE,CAAC;QAC1E,OAAO;YACL,GAAG,WAAW;YACd,OAAO,EAAE,cAAc;YACvB,SAAS,EAAE,wBAAwB;SACpC,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,2EAA2E;AAC3E,6EAA6E;AAC7E,SAAS,iBAAiB,CAAC,cAA8B;IACvD,MAAM,QAAQ,GAAG,cAAc,CAAC,iBACuB,CAAC;IACxD,MAAM,iBAAiB,GAAG,QAAQ;QAChC,CAAC,CAAC,CAAC,GAAmB,EAAE,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC3D,CAAC,CAAC,iBAAiB,CAAC;IACtB,OAAO,EAAE,GAAG,cAAc,EAAE,iBAAiB,EAAE,CAAC;AAClD,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,mFAAmF;AACnF,MAAM,uBAAuB,GAAsC;IACjE,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,MAAM;IACpB,eAAe,EAAE,KAAK;IACtB,aAAa,EAAE,KAAK;IACpB,aAAa,EAAE,KAAK;CACrB,CAAC;AAEF,0EAA0E;AAC1E,gFAAgF;AAChF,gDAAgD;AAChD,MAAM,UAAU,uBAAuB,CAAC,WAAmB;IACzD,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IAC7C,MAAM,KAAK,GAAG,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAClD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IACxB,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC/C,OAAO,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;AACvD,CAAC;AAED,gFAAgF;AAChF,iFAAiF;AACjF,SAAS,aAAa,CAAC,GAAW;IAChC,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;IACpD,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACxE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,+EAA+E;AAC/E,8EAA8E;AAC9E,4EAA4E;AAC5E,oDAAoD;AACpD,SAAS,oBAAoB,CAAC,QAAgB;IAI5C,MAAM,MAAM,GAAqB,EAAE,CAAC;IACpC,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;QAC7D,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QACxD,MAAM,GAAG,GAAG,uBAAuB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,GAAG,QAAQ,SAAS,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;QAC5D,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;AAClC,CAAC;AAED,+EAA+E;AAC/E,+EAA+E;AAC/E,gDAAgD;AAChD,KAAK,UAAU,qBAAqB,CAClC,YAA0B,EAC1B,OAAuB;IAMvB,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,MAAM,iBAAiB,GAAG,OAAO,CAAC,SAAS,KAAK,UAAU,CAAC;IAC3D,IAAI,cAAc,GAA+B,iBAAiB,CAChE,YAAY,CAAC,OAAO,CAAC,OAAqC,EAAE;QAC1D,iBAAiB;KAClB,CAAC,CACH,CAAC;IAEF,2EAA2E;IAC3E,sEAAsE;IACtE,IAAI,SAC6D,CAAC;IAClE,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,0EAA0E;QAC1E,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QAC1E,SAAS,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAC3C,cAAc,GAAG;YACf,GAAG,cAAc;YACjB,YAAY,EAAE,SAAS,CAAC,YAAY;SACrC,CAAC;IACJ,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,aAAa,CAC/C,YAAY,EACZ,cAAc,CACf,CAAC;IACF,MAAM,aAAa,GAAG,WAAW,CAAC,aAAa,CAAC,KAAK,EAAE;QACrD,WAAW,EAAE,OAAO,CAAC,MAAM,KAAK,OAAO;KACxC,CAAC,CAAC;IACH,MAAM,EAAE,GAAG,QAAQ,CACjB,aAAa,EACb,OAAO,CAAC,QAAQ,EAChB,iBAAiB,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAC/B,CAAC;IACF,MAAM,YAAY,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC;IACrC,wEAAwE;IACxE,MAAM,WAAW,GACf,OAAO,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAC7E,0EAA0E;IAC1E,2EAA2E;IAC3E,2DAA2D;IAC3D,MAAM,MAAM,GACV,OAAO,CAAC,aAAa,KAAK,SAAS;QACjC,CAAC,CAAC,6BAA6B,CAAC,WAAW,CAAC;QAC5C,CAAC,CAAC,WAAW,CAAC;IAClB,MAAM,WAAW,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC3C,OAAO;QACL,QAAQ,EAAE,WAAW;QACrB,QAAQ,EAAE,aAAa,CAAC,QAAQ;QAChC,MAAM,EAAE,SAAS,EAAE,MAAM;KAC1B,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,4EAA4E;AAC5E,8EAA8E;AAC9E,MAAM,2BAA2B,GAAG;IAClC,0BAA0B,EAAE,+BAA+B;IAC3D,UAAU,EAAE,yBAAyB;IACrC,eAAe,EAAE,4BAA4B;IAC7C,qBAAqB,EAAE,wBAAwB;IAC/C,qBAAqB,EAAE,0CAA0C;CAClE,CAAC;AAEF,SAAS,kBAAkB,CAAC,OAAe;IACzC,MAAM,KAAK,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACpC,OAAO,2BAA2B,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;AAChF,CAAC;AAED,0EAA0E;AAC1E,yDAAyD;AACzD,SAAS,uBAAuB,CAAC,KAAc,EAAE,QAAiB;IAChE,mCAAmC;IACnC,IAAI,KAAK,YAAY,mBAAmB,EAAE,CAAC;QACzC,MAAM,KAAK,CAAC;IACd,CAAC;IAED,wDAAwD;IACxD,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5E,MAAM,SAAS,GACb,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,IAAI,KAAK;QACnD,CAAC,CAAE,KAA0B,CAAC,IAAI;QAClC,CAAC,CAAC,SAAS,CAAC;IAEhB,2DAA2D;IAC3D,uEAAuE;IACvE,gCAAgC;IAChC,IACE,SAAS,KAAK,QAAQ;QACtB,SAAS,KAAK,SAAS;QACvB,SAAS,KAAK,OAAO,EACrB,CAAC;QACD,MAAM,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,yDAAyD;IACzD,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,uDAAuD;IACvD,IAAI,SAAS,KAAK,QAAQ,IAAI,SAAS,KAAK,OAAO,EAAE,CAAC;QACpD,MAAM,IAAI,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC1C,CAAC;IAED,yEAAyE;IACzE,2EAA2E;IAC3E,2EAA2E;IAC3E,8EAA8E;IAC9E,0EAA0E;IAC1E,6EAA6E;IAC7E,+BAA+B;IAC/B,IAAI,kBAAkB,CAAC,YAAY,CAAC,EAAE,CAAC;QACrC,qEAAqE;QACrE,iEAAiE;QACjE,MAAM,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,oDAAoD;IACpD,MAAM,IAAI,eAAe,CACvB,8GAA8G,EAC9G,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAC3C,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,0EAA0E;AAC1E,wEAAwE;AACxE,qEAAqE;AACrE,MAAM,UAAU,sBAAsB,CACpC,QAAmC;IAEnC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC3D,SAAS;QACX,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAE3C,gEAAgE;QAChE,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,SAAS;QACX,CAAC;QAED,qEAAqE;QACrE,MAAM,oBAAoB,GACxB,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YACxB,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC5B,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;YAC1B,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,OAAO,GAAG,kEAAkE,OAAO,CAAC,OAAO,IAAI,CAAC;QACtG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAClB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAA2B,EAC3B,UAA0B,EAAE;IAE5B,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAE/D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QAEtC,iEAAiE;QACjE,MAAM,UAAU,GAAG,MAAM,yBAAyB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;QAE9C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,qBAAqB,CAChE,MAAM,CAAC,YAAY,EACnB,OAAO,CACR,CAAC;QACF,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC,CAAC;QAEnD,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;IAC1E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAED,qDAAqD;AACrD,+EAA+E;AAC/E,2EAA2E;AAC3E,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO,CACnC,KAA2B,EAC3B,UAA0B,EAAE;IAE5B,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAE/D,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QAEtC,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,qBAAqB,CAC9C,MAAM,CAAC,YAAY,EACnB,OAAO,CACR,CAAC;QACF,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC","sourcesContent":["import TurndownService from '@joplin/turndown';\nimport * as turndownPluginGfm from '@joplin/turndown-plugin-gfm';\nimport * as mammoth from 'mammoth';\nimport * as markdownlint from 'markdownlint/sync';\nimport { applyFixes } from 'markdownlint';\nimport { parse, type HTMLElement } from 'node-html-parser';\nimport * as prettier from 'prettier';\nimport * as prettierMarkdown from 'prettier/plugins/markdown';\nimport JSZip from 'jszip';\nimport fs from 'fs/promises';\nimport path from 'path';\n\nexport interface ConvertOptions {\n mammoth?: object;\n turndown?: object;\n /**\n * How to handle images. `'inline'` (default) keeps Mammoth's base64 data\n * URIs; `'strip'` removes images entirely (useful to avoid multi-MB output\n * from image-heavy documents); `'extract'` replaces each image with a\n * relative `` link and returns the image bytes on\n * `ConvertResult.images` (use `convertWithWarnings` to retrieve them).\n */\n images?: 'inline' | 'strip' | 'extract';\n /**\n * Directory prefix used for extracted image links and paths (default\n * `'images'`). Only applies when `images` is `'extract'`. The same value is\n * used for the Markdown link (``) and the returned\n * `ExtractedImage.path`, so links and files always agree.\n */\n imageDir?: string;\n /**\n * How to render Word's numbered lists. `'ordered'` (default) keeps them as\n * `1.`/`2.`/… ordered lists; `'bullets'` converts them to bullet lists\n * (matching the classic word-to-markdown behavior).\n */\n numberedLists?: 'bullets' | 'ordered';\n /**\n * How to handle underlined text. `'ignore'` (default) drops the underline —\n * Mammoth's default, since underlines are easily confused with links in HTML.\n * `'preserve'` keeps it as an inline `<u>…</u>` tag (rendered by GitHub-flavored\n * Markdown). Note that superscript and subscript are always preserved as\n * `<sup>`/`<sub>` and need no option.\n */\n underline?: 'ignore' | 'preserve';\n /**\n * How to render Word's footnotes and endnotes. `'gfm'` (default) rewrites\n * Mammoth's superscript reference links plus trailing note list into standard\n * GitHub-flavored/Pandoc footnote syntax (`[^1]` references and `[^1]:`\n * definitions). `'preserve'` keeps Mammoth's raw `<sup>` links and numbered\n * note list (useful for CommonMark targets that don't support `[^1]`).\n */\n footnotes?: 'gfm' | 'preserve';\n}\n\n// Mammoth's options object, narrowed to the field we merge into. Mammoth appends\n// a provided styleMap to its default map, so adding an entry keeps the built-in\n// mappings (including superscript/subscript) intact.\ninterface MammothOptions {\n styleMap?: string[];\n [key: string]: unknown;\n}\n\n// An image pulled out of the document by `images: 'extract'`. `path` is the\n// relative link used in the Markdown (e.g. `images/image1.png`); `bytes` is the\n// raw file content for the caller to write to disk or bundle into a zip.\nexport interface ExtractedImage {\n path: string;\n contentType: string;\n bytes: Uint8Array;\n}\n\nexport interface ConvertResult {\n markdown: string;\n warnings: string[];\n /** Present (possibly empty) when converting with `images: 'extract'`. */\n images?: ExtractedImage[];\n}\n\nexport interface DocumentProperties {\n sensitivity?: string;\n confidentiality?: string;\n encryption?: boolean;\n protection?: boolean;\n}\n\n// Base class for every user-facing error the converter throws, so callers can\n// catch them all with one instanceof check\nexport class WordToMarkdownError extends Error {}\n\n// Custom error class for unsupported file formats\nexport class UnsupportedFileError extends WordToMarkdownError {\n constructor(message: string) {\n super(message);\n this.name = 'UnsupportedFileError';\n }\n}\n\n// Custom error class for file not found\nexport class FileNotFoundError extends WordToMarkdownError {\n constructor(filePath?: string) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `File not found${location}. Please check that the file exists and the path is correct.`,\n );\n this.name = 'FileNotFoundError';\n }\n}\n\n// Custom error class for invalid/corrupted files\nexport class InvalidFileError extends WordToMarkdownError {\n constructor(filePath?: string) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `Invalid file${location}. The file is not a valid .docx file or is corrupted. Please ensure the file is a valid Microsoft Word document (.docx format).`,\n );\n this.name = 'InvalidFileError';\n }\n}\n\n// Custom error class for permission errors\nexport class FilePermissionError extends WordToMarkdownError {\n constructor(filePath?: string) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `Permission denied${location}. Cannot read the file. Please check file permissions.`,\n );\n this.name = 'FilePermissionError';\n }\n}\n\n// Custom error class for general conversion errors\nexport class ConversionError extends WordToMarkdownError {\n constructor(message: string, originalError?: Error) {\n // Standard error chaining for better debugging tool support\n super(message, originalError ? { cause: originalError } : undefined);\n this.name = 'ConversionError';\n }\n}\n\ninterface turndownOptions {\n headingStyle?: 'setext' | 'atx';\n codeBlockStyle?: 'indented' | 'fenced';\n bulletListMarker?: '*' | '-' | '+';\n}\n\nconst defaultTurndownOptions: turndownOptions = {\n headingStyle: 'atx',\n codeBlockStyle: 'fenced',\n bulletListMarker: '-',\n};\n\n// Check if a file path has a .doc extension (unsupported format)\nexport function validateFileExtension(filePath: string): void {\n // Use manual extension parsing (works in both Node.js and browser)\n const filename = filePath.toLowerCase();\n const lastDotIndex = filename.lastIndexOf('.');\n const ext = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';\n\n if (ext === '.doc') {\n throw new UnsupportedFileError(\n 'This tool only supports .docx files, not .doc files. Please save your document as a .docx file and try again.',\n );\n }\n}\n\n// Validates that a file path is safe to use and returns the resolved path (Node.js only)\nfunction validateFilePath(filePath: string): string {\n // Resolve to an absolute path first. path.resolve normalizes any `..`\n // segments, so we deliberately do NOT reject paths that merely contain `..`:\n // a relative path like `../report.docx`, or a filename like `notes..docx`, is\n // legitimate CLI/library usage. (There is no sandbox to escape here — a local\n // caller already has full filesystem access.)\n // Note: the path module is Node.js-only, but this function is only called in\n // Node.js contexts (CLI, direct API use with file paths).\n const resolvedPath = path.resolve(filePath);\n\n // Check for absolute paths to dangerous system directories (Unix-like systems)\n const dangerousPaths = ['/etc/', '/sys/', '/proc/', '/root/', '/boot/'];\n for (const dangerousPath of dangerousPaths) {\n if (resolvedPath.startsWith(dangerousPath)) {\n throw new FilePermissionError(filePath);\n }\n }\n\n // Check for Windows system directories\n const windowsDangerousPaths = ['C:\\\\Windows\\\\', 'C:\\\\Program Files\\\\'];\n for (const dangerousPath of windowsDangerousPaths) {\n if (resolvedPath.toUpperCase().startsWith(dangerousPath.toUpperCase())) {\n throw new FilePermissionError(filePath);\n }\n }\n\n return resolvedPath;\n}\n\n// Turndown will add an empty header if the first row\n// of the table isn't `<th>` elements. This function\n// converts the first row of a table to `<th>` elements\n// so that it renders correctly in Markdown.\n// Common unicode bullets that might appear in Word documents - compiled once\nconst unicodeBullets = ['•', '◦', '▪', '▫', '‣', '⁃', '∙', '·'];\nconst bulletRegex = new RegExp(\n `^\\\\s*[${unicodeBullets.map((b) => b.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('')}]\\\\s*`,\n);\n\n// A row's own cells, excluding cells of any table nested inside it\nfunction rowCells(row: HTMLElement): HTMLElement[] {\n // Text nodes have no tagName, hence the optional chain\n return (row.childNodes as HTMLElement[]).filter(\n (node: HTMLElement) => node.tagName?.toLowerCase() === 'td',\n );\n}\n\n// Process HTML in a single pass: optionally strip images, convert table\n// headers, and remove unicode bullets. This is more efficient than parsing the\n// HTML twice.\n/** @internal Exported for tests only. */\nexport function processHtml(\n html: string,\n opts: { stripImages?: boolean } = {},\n): string {\n const root = parse(html);\n\n // Remove images (Mammoth inlines them as base64 data URIs by default, which\n // can bloat the output for image-heavy documents).\n if (opts.stripImages) {\n root.querySelectorAll('img').forEach((img: HTMLElement) => img.remove());\n }\n\n // Unwrap single-cell tables whose only content is a <pre> code block. Word and\n // LibreOffice commonly wrap a code block in a bordered 1×1 table (a shaded box);\n // left alone, Turndown would render the code as a one-cell Markdown table. This\n // must run before the header-promotion pass below, which would otherwise turn\n // the code cell's <td> into a <th> and mangle the block.\n root.querySelectorAll('table').forEach((table: HTMLElement) => {\n const rows = table.querySelectorAll('tr');\n if (rows.length !== 1) return;\n const cells = rows[0].querySelectorAll('td');\n if (cells.length !== 1) return;\n const cell = cells[0];\n const pres = cell.querySelectorAll('pre');\n // Only unwrap when the cell holds exactly one <pre> and nothing else of\n // substance (its text is entirely the code) — never strip a table that also\n // contains prose or other elements alongside the code.\n if (pres.length !== 1) return;\n if (cell.textContent.trim() !== pres[0].textContent.trim()) return;\n table.replaceWith(pres[0]);\n });\n\n // Process tables - convert first row to table headers\n root.querySelectorAll('table').forEach((table: HTMLElement) => {\n const firstRow = table.querySelector('tr');\n if (!firstRow) return;\n\n // If first row already has TH elements, leave it alone\n if (firstRow.querySelector('th')) return;\n\n // Check if first row is empty or has only empty cells. An image-only cell\n // isn't empty: dropping the row would drop the image.\n const cells = rowCells(firstRow);\n const isEmpty =\n cells.length === 0 ||\n cells.every(\n (cell: HTMLElement) =>\n !cell.textContent?.trim() && !cell.querySelector('img'),\n );\n\n if (isEmpty) {\n // Remove empty first row and find the first non-empty row to convert\n firstRow.remove();\n const nextRow = table.querySelector('tr');\n if (nextRow) {\n rowCells(nextRow).forEach((cell: HTMLElement) => {\n cell.tagName = 'th';\n });\n }\n } else {\n // Convert first row TD elements to TH\n cells.forEach((cell: HTMLElement) => {\n cell.tagName = 'th';\n });\n }\n });\n\n // Process lists - remove unicode bullets from unnumbered list items\n root.querySelectorAll('ul li').forEach((listItem: HTMLElement) => {\n // Get the text content and remove unicode bullets from the beginning\n const textContent = listItem.innerHTML;\n const cleanedContent = textContent.replace(bulletRegex, '');\n if (cleanedContent !== textContent) {\n listItem.innerHTML = cleanedContent;\n }\n });\n\n return root.toString();\n}\n\n// Reusable TurndownService instance to avoid recreating it for each conversion\nlet turndownServiceInstance: TurndownService | null = null;\n\nfunction getTurndownService(\n options: object = {},\n keepTags: string[] = [],\n): TurndownService {\n // Create a new instance if options or keep-tags are provided; otherwise reuse\n // the singleton. `keep()` mutates the instance, so it must never touch the\n // shared singleton — a fresh service is required whenever keepTags is set.\n if (Object.keys(options).length > 0 || keepTags.length > 0) {\n const service = new TurndownService({\n ...defaultTurndownOptions,\n ...options,\n });\n service.use(turndownPluginGfm.gfm);\n if (keepTags.length > 0) {\n service.keep(keepTags);\n }\n return service;\n }\n\n if (!turndownServiceInstance) {\n turndownServiceInstance = new TurndownService(defaultTurndownOptions);\n turndownServiceInstance.use(turndownPluginGfm.gfm);\n }\n return turndownServiceInstance;\n}\n\n// Convert HTML to GitHub-flavored Markdown. `keepTags` lists HTML tags to\n// preserve verbatim as inline HTML (e.g. `['u']` to keep underlines) rather\n// than let Turndown strip them to plain text.\nexport function htmlToMd(\n html: string,\n options: object = {},\n keepTags: string[] = [],\n): string {\n // Turndown's DOM parser decodes entities exactly once. Don't pre-decode:\n // that would turn literal text like `<b>` into markup and truncate\n // attribute values containing `"`.\n const turndownService = getTurndownService(options, keepTags);\n return turndownService.turndown(html).trim();\n}\n\n// Pre-compiled regex patterns for better performance\nconst numberedListRegex = /^(\\s*)(\\d+)\\.\\s/;\nconst fenceRegex = /^\\s*(`{3,}|~{3,})/;\nconst nonBreakingSpacesRegex = /[\\u00A0\\u2007\\u202F\\u2060\\uFEFF]/g;\nconst smartQuotesRegex = /[\\u201C\\u201D\\u2018\\u2019]/g;\n\n// Mammoth renders footnotes/endnotes as a superscript reference link plus a\n// trailing ordered list of note bodies with `\\u2191` backlinks \\u2014 not real Markdown\n// footnotes. These two regexes rewrite that into GFM/Pandoc footnote syntax.\n// Both anchor on Mammoth's stable anchor ids (`#footnote-N` / `#footnote-ref-N`,\n// or the `endnote` variants), never the escaped display label, which prettier\n// and markdownlint may re-escape.\n//\n// Reference in the body, e.g. `<sup>[\\[1\\]](#footnote-1)</sup>` \\u2192 `[^1]`. The\n// non-greedy link text backtracks past the escaped `\\]` inside the label, but\n// can't cross a `<`, so an earlier, unrelated `<sup>` link isn't swallowed.\nconst footnoteRefRegex = /<sup>\\[[^<]*?\\]\\(#(?:foot|end)note-(\\d+)\\)<\\/sup>/g;\n// Definition list item, e.g. `1. Body text. [\\u2191](#footnote-ref-1)` \\u2192\n// `[^1]: Body text.`. The list marker is unreliable (prettier renumbers), so the\n// footnote number comes from the backlink. Only single-line note bodies match:\n// a multi-paragraph body (a rare Word construct) puts the backlink on an indented\n// continuation line the (newline-free) body group can't reach, so it's left as-is\n// \\u2014 see convertFootnotes for how the matching reference is then also left raw.\nconst footnoteDefRegex =\n /^[ \\t]*\\d+\\.[ \\t]+(.*?)[ \\t]*\\[\\u2191\\]\\(#(?:foot|end)note-ref-(\\d+)\\)[ \\t]*$/gm;\n\n// Map for non-breaking space replacements\nconst nonBreakingSpaceMap: { [key: string]: string } = {\n '\\u00A0': ' ', // Non-breaking space\n '\\u2007': ' ', // Figure space\n '\\u202F': ' ', // Narrow no-break space\n '\\u2060': '', // Word joiner (zero-width non-breaking space)\n '\\uFEFF': '', // Zero-width no-break space (BOM)\n};\n\n// Map for smart quote replacements\nconst smartQuoteMap: { [key: string]: string } = {\n '\\u201C': '\"', // Left double quotation mark\n '\\u201D': '\"', // Right double quotation mark\n '\\u2018': \"'\", // Left single quotation mark\n '\\u2019': \"'\", // Right single quotation mark\n};\n\n// Convert numbered lists to bullet lists, leaving fenced code blocks untouched\nfunction convertNumberedListsToBullets(md: string): string {\n let fence: string | null = null;\n return md\n .split('\\n')\n .map((line) => {\n const marker = line.match(fenceRegex)?.[1];\n if (marker) {\n // A fence closes only with the same character, at least as long\n if (fence === null) fence = marker;\n else if (marker[0] === fence[0] && marker.length >= fence.length)\n fence = null;\n return line;\n }\n return fence === null ? line.replace(numberedListRegex, '$1- ') : line;\n })\n .join('\\n');\n}\n\n// Remove unicode non-breaking spaces and convert smart quotes to ASCII in a single pass\nfunction normalizeText(md: string): string {\n return md\n .replace(nonBreakingSpacesRegex, (char) => nonBreakingSpaceMap[char])\n .replace(smartQuotesRegex, (char) => smartQuoteMap[char]);\n}\n\n// Rewrite Mammoth's footnote/endnote markup into GFM/Pandoc footnote syntax.\n// See footnoteRefRegex / footnoteDefRegex for the shapes matched. Runs after\n// lint() and before prettify() so prettier normalizes the resulting footnote\n// block (prettier's markdown parser preserves `[^1]` / `[^1]:`).\nfunction convertFootnotes(md: string): string {\n // Rewrite the note definitions first, recording which footnote numbers were\n // actually converted. A number won't convert if its body spans multiple\n // paragraphs (footnoteDefRegex only matches single-line bodies).\n const converted = new Set<string>();\n const withDefs = md.replace(footnoteDefRegex, (_match, body, num) => {\n converted.add(num);\n return `[^${num}]: ${body}`;\n });\n // Only convert references whose definition converted. Rewriting a reference\n // whose definition was left as a raw list item would produce a dangling `[^N]`\n // with no target; gating on `converted` keeps that (rare) note as intact raw\n // `<sup>` + list markup instead.\n return withDefs.replace(footnoteRefRegex, (match, num) =>\n converted.has(num) ? `[^${num}]` : match,\n );\n}\n\n// Lint the Markdown and correct any issues\nfunction lint(md: string): string {\n const lintResult = markdownlint.lint({ strings: { md } });\n return applyFixes(md, lintResult['md']).trim();\n}\n\n// Format the Markdown with Prettier\nasync function prettify(md: string): Promise<string> {\n const formatted = await prettier.format(md, {\n parser: 'markdown',\n plugins: [prettierMarkdown],\n });\n return formatted.trim();\n}\n\n// Extract document properties from a .docx file\nexport async function extractDocumentProperties(\n input: string | ArrayBuffer,\n): Promise<DocumentProperties> {\n const properties: DocumentProperties = {};\n\n try {\n let arrayBuffer: ArrayBuffer;\n if (typeof input === 'string') {\n // Validate the file path to prevent path traversal attacks\n const safePath = validateFilePath(input);\n\n // Read file from path and convert to ArrayBuffer\n const fileBuffer = await fs.readFile(safePath);\n arrayBuffer = toArrayBuffer(\n fileBuffer.buffer.slice(\n fileBuffer.byteOffset,\n fileBuffer.byteOffset + fileBuffer.byteLength,\n ),\n );\n } else {\n arrayBuffer = input;\n }\n\n const zip = await JSZip.loadAsync(arrayBuffer);\n\n // Check for encryption - encrypted files have EncryptionInfo and EncryptedPackage\n const encryptionInfo = zip.file('EncryptionInfo');\n if (encryptionInfo) {\n properties.encryption = true;\n }\n\n // Try to read core properties\n const corePropsFile = zip.file('docProps/core.xml');\n if (corePropsFile) {\n const coreXml = await corePropsFile.async('string');\n // Look for keywords that might indicate sensitivity/confidentiality\n if (\n coreXml.toLowerCase().includes('confidential') ||\n coreXml.toLowerCase().includes('sensitive')\n ) {\n properties.confidentiality = 'detected in core properties';\n }\n }\n\n // Try to read custom properties\n const customPropsFile = zip.file('docProps/custom.xml');\n if (customPropsFile) {\n const customXml = await customPropsFile.async('string');\n const customXmlLower = customXml.toLowerCase();\n\n // Use regex patterns to detect sensitivity/confidentiality properties\n // We use regex instead of full XML parsing for performance and simplicity,\n // as we only need to detect the presence of specific property names, not extract values\n\n // Pattern matches common sensitivity/confidentiality property names:\n // - \"Sensitivity\" (standard Office property)\n // - \"MSIP_Label_*\" (Microsoft Information Protection labels)\n // - Any property with \"confidential\" or \"sensitive\" in the name\n // We only check for the property name attribute existence, not the full element content,\n // to avoid potential catastrophic backtracking on large/malformed XML\n const sensitivityPattern =\n /<property[^>]*\\bname=\"(?:Sensitivity|MSIP_Label_[^\"]*|[^\"]*(?:confidential|sensitive)[^\"]*)\"[^>]*>/gi;\n\n const hasSensitivityProperty = sensitivityPattern.test(customXml);\n const hasConfidentialText = customXmlLower.includes('confidential');\n const hasMSIPLabel = customXmlLower.includes('msip_label');\n\n if (hasSensitivityProperty || hasMSIPLabel) {\n properties.sensitivity = 'detected in custom properties';\n }\n\n if (hasConfidentialText) {\n properties.confidentiality = 'detected in custom properties';\n }\n }\n\n // Check for document protection\n const settingsFile = zip.file('word/settings.xml');\n if (settingsFile) {\n const settingsXml = await settingsFile.async('string');\n if (\n settingsXml.includes('<w:documentProtection') ||\n settingsXml.includes('<w:writeProtection')\n ) {\n properties.protection = true;\n }\n }\n } catch (error) {\n // If we can't extract properties, just continue without them\n // This might happen with encrypted, corrupted, or non-standard .docx files\n // We log the error in development mode but don't fail the conversion\n // `process` doesn't exist in the browser worker unless a bundler shims it\n if (\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV === 'development'\n ) {\n console.warn('Failed to extract document properties:', error);\n }\n }\n\n return properties;\n}\n\n// Generate warnings based on document properties\nexport function generateWarnings(properties: DocumentProperties): string[] {\n const warnings: string[] = [];\n\n if (properties.encryption) {\n warnings.push(\n 'Warning: This document appears to be encrypted. Conversion may not include all content or may fail entirely.',\n );\n }\n\n if (properties.sensitivity) {\n warnings.push(\n `Warning: This document has sensitivity labels (${properties.sensitivity}). Please ensure you have permission to convert and share this content.`,\n );\n }\n\n if (properties.confidentiality) {\n warnings.push(\n `Warning: This document contains confidentiality markers (${properties.confidentiality}). Please verify that conversion is authorized.`,\n );\n }\n\n if (properties.protection) {\n warnings.push(\n 'Warning: This document has editing restrictions enabled. Some content may not convert properly.',\n );\n }\n\n return warnings;\n}\n\n// The input shapes mammoth accepts across environments\ntype MammothInput = { buffer: Buffer } | { arrayBuffer: ArrayBuffer };\n\n// Encrypted (password-protected) .docx files and legacy .doc files are OLE\n// Compound File Binary containers, not ZIPs. They start with this signature.\nconst CFB_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];\n\nfunction isCompoundFile(bytes: ArrayBuffer): boolean {\n const head = new Uint8Array(bytes, 0, Math.min(bytes.byteLength, 8));\n return (\n head.length === CFB_SIGNATURE.length &&\n CFB_SIGNATURE.every((byte, i) => head[i] === byte)\n );\n}\n\ninterface LoadedInput {\n bytes: ArrayBuffer;\n mammothInput: MammothInput;\n}\n\n// Read the input once and shape it for mammoth. In Node.js, mammoth's unzip\n// only accepts { path | buffer | file }; the browser build takes { arrayBuffer }.\nasync function loadInput(input: string | ArrayBuffer): Promise<LoadedInput> {\n let bytes: ArrayBuffer;\n if (typeof input === 'string') {\n validateFileExtension(input);\n // Validate the file path to prevent path traversal attacks\n const fileBuffer = await fs.readFile(validateFilePath(input));\n bytes = toArrayBuffer(\n fileBuffer.buffer.slice(\n fileBuffer.byteOffset,\n fileBuffer.byteOffset + fileBuffer.byteLength,\n ),\n );\n } else {\n bytes = input;\n }\n\n if (isCompoundFile(bytes)) {\n throw new UnsupportedFileError(\n 'This file is password-protected or is a legacy .doc file. Please remove the password or save it as a .docx file and try again.',\n );\n }\n\n const mammothInput: MammothInput =\n typeof Buffer !== 'undefined'\n ? { buffer: Buffer.from(bytes) }\n : { arrayBuffer: bytes };\n return { bytes, mammothInput };\n}\n\n// A conversion message emitted by mammoth (e.g. dropped/unsupported content)\ninterface MammothMessage {\n type: string;\n message: string;\n}\n\n// Ensure we have an ArrayBuffer (not a SharedArrayBuffer) by copying if needed\nfunction toArrayBuffer(buffer: ArrayBufferLike): ArrayBuffer {\n if (buffer instanceof ArrayBuffer) {\n return buffer;\n }\n const uint8Array = new Uint8Array(buffer);\n const newArrayBuffer = new ArrayBuffer(uint8Array.byteLength);\n new Uint8Array(newArrayBuffer).set(uint8Array);\n return newArrayBuffer;\n}\n\n// Synthetic paragraph-style name applied to code paragraphs detected by font\n// (see tagCodeParagraphs). It's mapped to a fenced code block alongside the real\n// Word/LibreOffice preformatted styles below.\nconst DETECTED_CODE_STYLE_NAME = 'W2M Code Block';\n\n// Word and LibreOffice mark code blocks in two ways, both of which we route to a\n// single `<pre><code>`:\n// - a dedicated paragraph style — \"Preformatted Text\" (LibreOffice) or \"HTML\n// Preformatted\" (Word) — one paragraph per line; and\n// - direct monospace-font runs on otherwise-Normal paragraphs (often inside a\n// shaded 1×1 table), which tagCodeParagraphs re-labels with the synthetic\n// style name above.\n// Mapping each style to `pre > code:separator('\\n')` tells Mammoth to merge the\n// consecutive lines into one `<pre><code>` joined by newlines, which Turndown\n// emits as a single fenced block with verbatim content — no per-line paragraphs\n// and, crucially, no Markdown escaping of code punctuation (`[ ] { } * - /`). The\n// `\\n` in `separator` is a literal backslash-n in the source string; Mammoth's\n// style-map parser interprets it as a newline.\nconst CODE_BLOCK_STYLE_MAP = [\n `p[style-name='${DETECTED_CODE_STYLE_NAME}'] => pre > code:separator('\\\\n')`,\n \"p[style-name='Preformatted Text'] => pre > code:separator('\\\\n')\",\n \"p[style-name='HTML Preformatted'] => pre > code:separator('\\\\n')\",\n];\n\n// Append our style-map additions to any caller-supplied Mammoth options: the\n// code-block mappings always, plus `u => u` when underlines are being preserved\n// (Mammoth ignores underlines unless the style map maps them to a `<u>`).\n// Additions are appended, never replaced, so Mammoth's default mappings —\n// including superscript/subscript — stay intact.\nfunction withStyleMap(\n mammothOptions: MammothOptions | undefined,\n { preserveUnderline }: { preserveUnderline: boolean },\n): MammothOptions {\n const existing = mammothOptions?.styleMap ?? [];\n const additions = [...CODE_BLOCK_STYLE_MAP];\n if (preserveUnderline) additions.push('u => u');\n return { ...mammothOptions, styleMap: [...existing, ...additions] };\n}\n\n// Minimal shape of the nodes in Mammoth's document model that we walk. Mammoth\n// ships no types for this, so we narrow to just the fields we read: a node's\n// `type`, a text node's `value`, a run's `font` (the `w:rFonts` ascii name), and\n// child nodes.\ninterface MammothDocNode {\n type: string;\n value?: string;\n font?: string | null;\n children?: MammothDocNode[];\n [key: string]: unknown;\n}\n\n// Font names (lower-cased) treated as monospace/code fonts. The word-boundaried\n// `mono` matches families like \"Liberation Mono\"/\"DejaVu Sans Mono\" without\n// catching proportional fonts that merely start with those letters (e.g.\n// \"Monotype Corsiva\"); the rest are common fixed-width families.\nconst MONOSPACE_FONT_RE =\n /(^|\\s)mono(\\s|$)|monospace|consolas|courier|menlo|monaco|inconsolata/;\n\nfunction isMonospaceFont(font: string | null | undefined): boolean {\n return !!font && MONOSPACE_FONT_RE.test(font.toLowerCase());\n}\n\n// Concatenate all text under a node (runs, hyperlinks, etc.).\nfunction docNodeText(node: MammothDocNode): string {\n if (node.type === 'text') return node.value ?? '';\n return (node.children ?? []).map(docNodeText).join('');\n}\n\n// Collect every run descendant of a node (runs can be nested inside hyperlinks).\nfunction collectRuns(\n node: MammothDocNode,\n out: MammothDocNode[] = [],\n): MammothDocNode[] {\n if (node.type === 'run') out.push(node);\n (node.children ?? []).forEach((child) => collectRuns(child, out));\n return out;\n}\n\n// A paragraph reads as a code block when it has visible text and *every*\n// text-bearing run uses a monospace font. Requiring all runs (not just one) to\n// be monospace keeps ordinary prose that merely mentions a monospaced identifier\n// out of code blocks — only wholly-monospace paragraphs qualify.\nfunction isMonospaceParagraph(paragraph: MammothDocNode): boolean {\n const runs = collectRuns(paragraph).filter(\n (run) => docNodeText(run).trim().length > 0,\n );\n return runs.length > 0 && runs.every((run) => isMonospaceFont(run.font));\n}\n\n// A Mammoth `transformDocument` that recursively re-labels wholly-monospace\n// paragraphs with DETECTED_CODE_STYLE_NAME so the style map turns them into\n// fenced code blocks. Word often encodes code as monospace runs on Normal\n// paragraphs (no code paragraph style), and Mammoth discards font information\n// once it emits HTML — the document model is the only place the signal survives.\nfunction tagCodeParagraphs(node: MammothDocNode): MammothDocNode {\n const transformed: MammothDocNode = node.children\n ? { ...node, children: node.children.map(tagCodeParagraphs) }\n : node;\n if (transformed.type === 'paragraph' && isMonospaceParagraph(transformed)) {\n return {\n ...transformed,\n styleId: 'W2MCodeBlock',\n styleName: DETECTED_CODE_STYLE_NAME,\n };\n }\n return transformed;\n}\n\n// Add the code-detection transform to any caller-supplied Mammoth options,\n// composing with (running after) an existing `transformDocument` if present.\nfunction withCodeTransform(mammothOptions: MammothOptions): MammothOptions {\n const existing = mammothOptions.transformDocument as\n ((doc: MammothDocNode) => MammothDocNode) | undefined;\n const transformDocument = existing\n ? (doc: MammothDocNode) => tagCodeParagraphs(existing(doc))\n : tagCodeParagraphs;\n return { ...mammothOptions, transformDocument };\n}\n\n// Common image content types → file extension. Word most often embeds PNG and\n// JPEG; the rest cover formats Mammoth may surface. EMF/WMF are extracted as\n// bytes for completeness but browsers can't render them (Mammoth won't transcode).\nconst CONTENT_TYPE_EXTENSIONS: { [contentType: string]: string } = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/tiff': 'tiff',\n 'image/bmp': 'bmp',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n 'image/x-emf': 'emf',\n 'image/x-wmf': 'wmf',\n};\n\n// Pick a file extension for an extracted image. Falls back to the content\n// type's subtype when it's a clean alphanumeric token, else `bin`. Exported for\n// unit testing since only a PNG fixture exists.\nexport function extensionForContentType(contentType: string): string {\n const normalized = contentType.toLowerCase();\n const known = CONTENT_TYPE_EXTENSIONS[normalized];\n if (known) return known;\n const subtype = normalized.split('/')[1] ?? '';\n return /^[a-z0-9]+$/.test(subtype) ? subtype : 'bin';\n}\n\n// Decode a base64 string to bytes in both Node (Buffer) and the browser (atob).\n// Mammoth's `image.read('base64')` is the one encoding available in every build.\nfunction base64ToBytes(b64: string): Uint8Array {\n if (typeof Buffer !== 'undefined') {\n return new Uint8Array(Buffer.from(b64, 'base64'));\n }\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n// Build a Mammoth `convertImage` handler that pulls each image out into a byte\n// array with a deterministic relative path (`imageDir/imageN.ext`) instead of\n// inlining it as a base64 data URI. The collected images are exposed on the\n// returned `images` array once conversion finishes.\nfunction createImageExtractor(imageDir: string): {\n images: ExtractedImage[];\n convertImage: unknown;\n} {\n const images: ExtractedImage[] = [];\n const convertImage = mammoth.images.imgElement(async (image) => {\n const bytes = base64ToBytes(await image.read('base64'));\n const ext = extensionForContentType(image.contentType);\n const path = `${imageDir}/image${images.length + 1}.${ext}`;\n images.push({ path, contentType: image.contentType, bytes });\n return { src: path };\n });\n return { images, convertImage };\n}\n\n// The shared conversion pipeline: mammoth HTML -> cleaned, formatted Markdown.\n// Returns mammoth's messages so callers can surface content-loss warnings, and\n// (in extract mode) the extracted image assets.\nasync function runConversionPipeline(\n mammothInput: MammothInput,\n options: ConvertOptions,\n): Promise<{\n markdown: string;\n messages: MammothMessage[];\n images?: ExtractedImage[];\n}> {\n // Preserving underline requires cooperation at both ends of the pipeline:\n // Mammoth must be told to emit `<u>` (it drops underlines by default), and\n // Turndown must be told to keep that tag rather than flatten it to text.\n const preserveUnderline = options.underline === 'preserve';\n let mammothOptions: MammothOptions | undefined = withCodeTransform(\n withStyleMap(options.mammoth as MammothOptions | undefined, {\n preserveUnderline,\n }),\n );\n\n // Extract mode swaps Mammoth's default base64 inliner for a collector that\n // returns each image's bytes and rewrites the src to a relative path.\n let extractor:\n { images: ExtractedImage[]; convertImage: unknown } | undefined;\n if (options.images === 'extract') {\n // Drop trailing slashes so `img/` doesn't produce `img//image1.png` links\n const imageDir = (options.imageDir ?? 'images').replace(/(?<=.)\\/+$/, '');\n extractor = createImageExtractor(imageDir);\n mammothOptions = {\n ...mammothOptions,\n convertImage: extractor.convertImage,\n };\n }\n\n const mammothResult = await mammoth.convertToHtml(\n mammothInput,\n mammothOptions,\n );\n const processedHtml = processHtml(mammothResult.value, {\n stripImages: options.images === 'strip',\n });\n const md = htmlToMd(\n processedHtml,\n options.turndown,\n preserveUnderline ? ['u'] : [],\n );\n const normalizedMd = normalizeText(md);\n const cleanedMd = lint(normalizedMd);\n // Footnotes stay as GFM `[^1]` by default; skip the rewrite on request.\n const footnotedMd =\n options.footnotes === 'preserve' ? cleanedMd : convertFootnotes(cleanedMd);\n // Numbered lists stay numbered by default; flatten to bullets on request.\n // This must run after convertFootnotes, whose definition regex matches the\n // numbered `1. body [\\u2191](#footnote-ref-1)` list items.\n const listMd =\n options.numberedLists === 'bullets'\n ? convertNumberedListsToBullets(footnotedMd)\n : footnotedMd;\n const formattedMd = await prettify(listMd);\n return {\n markdown: formattedMd,\n messages: mammothResult.messages,\n images: extractor?.images,\n };\n}\n\n// Substrings (lower-cased) that, in a thrown error's message, indicate the\n// input isn't a readable .docx (invalid/corrupt/truncated ZIP, or a missing\n// required part). Sourced from JSZip and mammoth, which throw untyped Errors.\nconst INVALID_DOCX_ERROR_PATTERNS = [\n 'end of central directory', // JSZip: invalid ZIP structure\n 'zip file', // JSZip: not a valid ZIP\n 'corrupted zip', // JSZip: corrupted ZIP file\n 'end of data reached', // JSZip: truncated file\n 'could not find file', // mammoth: missing required file in .docx\n];\n\nfunction isInvalidDocxError(message: string): boolean {\n const lower = message.toLowerCase();\n return INVALID_DOCX_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));\n}\n\n// Translate the errors thrown by the pipeline into our typed, user-facing\n// error classes. Always throws (never returns normally).\nfunction classifyConversionError(error: unknown, filePath?: string): never {\n // Re-throw our custom errors as-is\n if (error instanceof WordToMarkdownError) {\n throw error;\n }\n\n // Handle specific error types from underlying libraries\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode =\n error && typeof error === 'object' && 'code' in error\n ? (error as { code: string }).code\n : undefined;\n\n // File not found errors (only occur with file path inputs)\n // ENOTDIR/ELOOP mean a path component isn't a usable directory, so the\n // file can't be reached either.\n if (\n errorCode === 'ENOENT' ||\n errorCode === 'ENOTDIR' ||\n errorCode === 'ELOOP'\n ) {\n throw new FileNotFoundError(filePath);\n }\n\n // A directory was passed where a .docx file was expected\n if (errorCode === 'EISDIR') {\n throw new InvalidFileError(filePath);\n }\n\n // Permission errors (only occur with file path inputs)\n if (errorCode === 'EACCES' || errorCode === 'EPERM') {\n throw new FilePermissionError(filePath);\n }\n\n // Invalid .docx file errors (from JSZip or mammoth during file parsing).\n // JSZip/mammoth throw plain, untyped Errors with no error code, so message\n // matching is the only signal available. Matching is case-insensitive so a\n // minor wording/capitalization change upstream is less likely to slip through\n // (see INVALID_DOCX_ERROR_PATTERNS). There is a small theoretical risk of\n // matching document content, but these technical phrases are highly unlikely\n // to appear in normal content.\n if (isInvalidDocxError(errorMessage)) {\n // Note: For ArrayBuffer inputs (e.g., web uploads), filePath will be\n // undefined, so the message won't include the original filename.\n throw new InvalidFileError(filePath);\n }\n\n // Wrap other errors with a general conversion error\n throw new ConversionError(\n 'An error occurred while converting the document. Please ensure the file is a valid .docx file and try again.',\n error instanceof Error ? error : undefined,\n );\n}\n\n// Turn mammoth's conversion messages into user-facing warnings. Mammoth is\n// chatty and emits many cosmetic \"unrecognised style\" notices on ordinary\n// documents; we surface only messages that indicate actual content loss\n// (dropped/ignored elements or unconvertible images), de-duplicated.\nexport function extractMammothWarnings(\n messages: readonly MammothMessage[],\n): string[] {\n const seen = new Set<string>();\n const warnings: string[] = [];\n\n for (const message of messages) {\n if (message.type !== 'warning' && message.type !== 'error') {\n continue;\n }\n\n const text = message.message.toLowerCase();\n\n // Skip cosmetic style-mapping notices — they don't drop content\n if (text.includes('style')) {\n continue;\n }\n\n // Surface only messages that signal dropped or unconvertible content\n const indicatesContentLoss =\n text.includes('ignored') ||\n text.includes('unsupported') ||\n text.includes('could not') ||\n text.includes('image');\n if (!indicatesContentLoss) {\n continue;\n }\n\n const warning = `Warning: Some document content may not have converted cleanly (${message.message}).`;\n if (!seen.has(warning)) {\n seen.add(warning);\n warnings.push(warning);\n }\n }\n\n return warnings;\n}\n\n// Converts a Word document to crisp, clean Markdown with warnings\nexport async function convertWithWarnings(\n input: string | ArrayBuffer,\n options: ConvertOptions = {},\n): Promise<ConvertResult> {\n const filePath = typeof input === 'string' ? input : undefined;\n\n try {\n const loaded = await loadInput(input);\n\n // Extract document properties to check for confidentiality flags\n const properties = await extractDocumentProperties(loaded.bytes);\n const warnings = generateWarnings(properties);\n\n const { markdown, messages, images } = await runConversionPipeline(\n loaded.mammothInput,\n options,\n );\n warnings.push(...extractMammothWarnings(messages));\n\n return images ? { markdown, warnings, images } : { markdown, warnings };\n } catch (error) {\n classifyConversionError(error, filePath);\n }\n}\n\n// Converts a Word document to crisp, clean Markdown.\n// Unlike convertWithWarnings, this skips document-property extraction (and its\n// extra ZIP parse) so callers that only need the Markdown pay no overhead.\nexport default async function convert(\n input: string | ArrayBuffer,\n options: ConvertOptions = {},\n): Promise<string> {\n const filePath = typeof input === 'string' ? input : undefined;\n\n try {\n const loaded = await loadInput(input);\n\n const { markdown } = await runConversionPipeline(\n loaded.mammothInput,\n options,\n );\n return markdown;\n } catch (error) {\n classifyConversionError(error, filePath);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["import TurndownService from '@joplin/turndown';\nimport * as turndownPluginGfm from '@joplin/turndown-plugin-gfm';\nimport * as mammoth from 'mammoth';\nimport * as markdownlint from 'markdownlint/sync';\nimport { applyFixes } from 'markdownlint';\nimport { parse, type HTMLElement } from 'node-html-parser';\nimport * as prettier from 'prettier';\nimport * as prettierMarkdown from 'prettier/plugins/markdown';\nimport JSZip from 'jszip';\nimport fs from 'fs/promises';\nimport path from 'path';\n\nexport interface ConvertOptions {\n mammoth?: object;\n turndown?: object;\n /**\n * How to handle images. `'inline'` (default) keeps Mammoth's base64 data\n * URIs; `'strip'` removes images entirely (useful to avoid multi-MB output\n * from image-heavy documents); `'extract'` replaces each image with a\n * relative `` link and returns the image bytes on\n * `ConvertResult.images` (use `convertWithWarnings` to retrieve them).\n */\n images?: 'inline' | 'strip' | 'extract';\n /**\n * Directory prefix used for extracted image links and paths (default\n * `'images'`). Only applies when `images` is `'extract'`. The same value is\n * used for the Markdown link (``) and the returned\n * `ExtractedImage.path`, so links and files always agree.\n */\n imageDir?: string;\n /**\n * How to render Word's numbered lists. `'ordered'` (default) keeps them as\n * `1.`/`2.`/… ordered lists; `'bullets'` converts them to bullet lists\n * (matching the classic word-to-markdown behavior).\n */\n numberedLists?: 'bullets' | 'ordered';\n /**\n * How to handle underlined text. `'ignore'` (default) drops the underline —\n * Mammoth's default, since underlines are easily confused with links in HTML.\n * `'preserve'` keeps it as an inline `<u>…</u>` tag (rendered by GitHub-flavored\n * Markdown). Note that superscript and subscript are always preserved as\n * `<sup>`/`<sub>` and need no option.\n */\n underline?: 'ignore' | 'preserve';\n /**\n * How to render Word's footnotes and endnotes. `'gfm'` (default) rewrites\n * Mammoth's superscript reference links plus trailing note list into standard\n * GitHub-flavored/Pandoc footnote syntax (`[^1]` references and `[^1]:`\n * definitions). `'preserve'` keeps Mammoth's raw `<sup>` links and numbered\n * note list (useful for CommonMark targets that don't support `[^1]`).\n */\n footnotes?: 'gfm' | 'preserve';\n}\n\n// Mammoth's options object, narrowed to the field we merge into. Mammoth appends\n// a provided styleMap to its default map, so adding an entry keeps the built-in\n// mappings (including superscript/subscript) intact.\ninterface MammothOptions {\n styleMap?: string[];\n [key: string]: unknown;\n}\n\n// An image pulled out of the document by `images: 'extract'`. `path` is the\n// relative link used in the Markdown (e.g. `images/image1.png`); `bytes` is the\n// raw file content for the caller to write to disk or bundle into a zip.\nexport interface ExtractedImage {\n path: string;\n contentType: string;\n bytes: Uint8Array;\n}\n\nexport interface ConvertResult {\n markdown: string;\n warnings: string[];\n /** Present (possibly empty) when converting with `images: 'extract'`. */\n images?: ExtractedImage[];\n}\n\nexport interface DocumentProperties {\n sensitivity?: string;\n confidentiality?: string;\n encryption?: boolean;\n protection?: boolean;\n}\n\n// Base class for every user-facing error the converter throws, so callers can\n// catch them all with one instanceof check\nexport class WordToMarkdownError extends Error {\n constructor(message?: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n// Custom error class for unsupported file formats\nexport class UnsupportedFileError extends WordToMarkdownError {\n constructor(message: string) {\n super(message);\n this.name = 'UnsupportedFileError';\n }\n}\n\n// Custom error class for file not found\nexport class FileNotFoundError extends WordToMarkdownError {\n constructor(filePath?: string) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `File not found${location}. Please check that the file exists and the path is correct.`,\n );\n this.name = 'FileNotFoundError';\n }\n}\n\n// Custom error class for invalid/corrupted files\nexport class InvalidFileError extends WordToMarkdownError {\n constructor(filePath?: string, cause?: unknown) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `Invalid file${location}. The file is not a valid .docx file or is corrupted. Please ensure the file is a valid Microsoft Word document (.docx format).`,\n cause === undefined ? undefined : { cause },\n );\n this.name = 'InvalidFileError';\n }\n}\n\n// Custom error class for permission errors\nexport class FilePermissionError extends WordToMarkdownError {\n constructor(filePath?: string) {\n const location = filePath ? `: \"${filePath}\"` : '';\n super(\n `Permission denied${location}. Cannot read the file. Please check file permissions.`,\n );\n this.name = 'FilePermissionError';\n }\n}\n\n// Custom error class for general conversion errors\nexport class ConversionError extends WordToMarkdownError {\n constructor(message: string, originalError?: Error) {\n // Standard error chaining for better debugging tool support\n super(message, originalError ? { cause: originalError } : undefined);\n this.name = 'ConversionError';\n }\n}\n\ninterface turndownOptions {\n headingStyle?: 'setext' | 'atx';\n codeBlockStyle?: 'indented' | 'fenced';\n bulletListMarker?: '*' | '-' | '+';\n}\n\nconst defaultTurndownOptions: turndownOptions = {\n headingStyle: 'atx',\n codeBlockStyle: 'fenced',\n bulletListMarker: '-',\n};\n\n// Check if a file path has a .doc extension (unsupported format)\nexport function validateFileExtension(filePath: string): void {\n // Use manual extension parsing (works in both Node.js and browser)\n const filename = filePath.toLowerCase();\n const lastDotIndex = filename.lastIndexOf('.');\n const ext = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';\n\n if (ext === '.doc') {\n throw new UnsupportedFileError(\n 'This tool only supports .docx files, not .doc files. Please save your document as a .docx file and try again.',\n );\n }\n}\n\n// Read a .docx from disk into a standalone ArrayBuffer (Node.js only). The\n// path is resolved as-is: a local caller already has full filesystem access, so\n// there's nothing to sandbox. Paths containing `..` (`../report.docx`,\n// `notes..docx`) are legitimate, and the OS enforces permissions (EACCES maps\n// to FilePermissionError in classifyConversionError).\nasync function readFileBytes(filePath: string): Promise<ArrayBuffer> {\n const fileBuffer = await fs.readFile(path.resolve(filePath));\n // Copy out of Node's pooled Buffer so the bytes don't alias unrelated data.\n // readFile never returns a SharedArrayBuffer-backed Buffer, hence the cast.\n return fileBuffer.buffer.slice(\n fileBuffer.byteOffset,\n fileBuffer.byteOffset + fileBuffer.byteLength,\n ) as ArrayBuffer;\n}\n\n// Common unicode bullets that might appear in Word documents - compiled once\nconst unicodeBullets = ['•', '◦', '▪', '▫', '‣', '⁃', '∙', '·'];\nconst bulletRegex = new RegExp(\n `^\\\\s*[${unicodeBullets.map((b) => b.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')).join('')}]\\\\s*`,\n);\n\n// A row's own cells, excluding cells of any table nested inside it\nfunction rowCells(row: HTMLElement): HTMLElement[] {\n // Text nodes have no tagName, hence the optional chain\n return (row.childNodes as HTMLElement[]).filter(\n (node: HTMLElement) => node.tagName?.toLowerCase() === 'td',\n );\n}\n\n// Mammoth renders footnotes/endnotes as a trailing <ol> whose items carry ids\n// like `footnote-1` / `endnote-1`.\nconst NOTE_ITEM_ID = /^(?:foot|end)note-\\d+$/;\n\nfunction isNoteList(list: HTMLElement): boolean {\n return (list.childNodes as HTMLElement[]).some(\n (node: HTMLElement) =>\n node.tagName?.toLowerCase() === 'li' &&\n NOTE_ITEM_ID.test(node.getAttribute('id') ?? ''),\n );\n}\n\n// Link schemes allowed through to the Markdown. Fragments and relative URLs\n// resolve against an https: base below, so they pass too.\nconst SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);\n// Browsers (via the WHATWG URL parser) ignore leading and trailing C0 controls\n// and spaces, so `\\x01javascript:` is still a `javascript:` URL. String#trim()\n// doesn't remove them, so strip them explicitly along with other whitespace.\n// eslint-disable-next-line no-control-regex\nconst EDGE_CONTROLS_REGEX = /^[\\s\\u0000-\\u001F]+|[\\s\\u0000-\\u001F]+$/g;\n\n// Returns the cleaned href if it's safe to keep, or null if the link should be\n// unwrapped to its text. Parsing with the WHATWG URL parser (available in Node\n// and browsers) determines the scheme exactly as a browser would, including\n// tabs/newlines inside the scheme (`java\\tscript:`) and any letter case.\nfunction safeHref(href: string): string | null {\n const trimmed = href.replace(EDGE_CONTROLS_REGEX, '');\n let url: URL;\n try {\n url = new URL(trimmed, 'https://base.invalid/');\n } catch {\n return null;\n }\n return SAFE_LINK_PROTOCOLS.has(url.protocol) ? trimmed : null;\n}\n\n// Process HTML in a single pass: optionally strip images, convert table\n// headers, and remove unicode bullets. This is more efficient than parsing the\n// HTML twice.\n/** @internal Exported for tests only. */\nexport function processHtml(\n html: string,\n opts: { stripImages?: boolean; bulletLists?: boolean } = {},\n): string {\n const root = parse(html);\n\n // Remove images (Mammoth inlines them as base64 data URIs by default, which\n // can bloat the output for image-heavy documents).\n if (opts.stripImages) {\n root.querySelectorAll('img').forEach((img: HTMLElement) => img.remove());\n }\n\n // Unwrap single-cell tables whose only content is a <pre> code block. Word and\n // LibreOffice commonly wrap a code block in a bordered 1×1 table (a shaded box);\n // left alone, Turndown would render the code as a one-cell Markdown table. This\n // must run before the header-promotion pass below, which would otherwise turn\n // the code cell's <td> into a <th> and mangle the block.\n root.querySelectorAll('table').forEach((table: HTMLElement) => {\n const rows = table.querySelectorAll('tr');\n if (rows.length !== 1) return;\n const cells = rows[0].querySelectorAll('td');\n if (cells.length !== 1) return;\n const cell = cells[0];\n const pres = cell.querySelectorAll('pre');\n // Only unwrap when the cell holds exactly one <pre> and nothing else of\n // substance (its text is entirely the code) — never strip a table that also\n // contains prose or other elements alongside the code.\n if (pres.length !== 1) return;\n if (cell.textContent.trim() !== pres[0].textContent.trim()) return;\n table.replaceWith(pres[0]);\n });\n\n // Process tables - convert first row to table headers\n root.querySelectorAll('table').forEach((table: HTMLElement) => {\n const firstRow = table.querySelector('tr');\n if (!firstRow) return;\n\n // If first row already has TH elements, leave it alone\n if (firstRow.querySelector('th')) return;\n\n // Check if first row is empty or has only empty cells. An image-only cell\n // isn't empty: dropping the row would drop the image.\n const cells = rowCells(firstRow);\n const isEmpty =\n cells.length === 0 ||\n cells.every(\n (cell: HTMLElement) =>\n !cell.textContent?.trim() && !cell.querySelector('img'),\n );\n\n if (isEmpty) {\n // Remove empty first row and find the first non-empty row to convert\n firstRow.remove();\n const nextRow = table.querySelector('tr');\n if (nextRow) {\n rowCells(nextRow).forEach((cell: HTMLElement) => {\n cell.tagName = 'th';\n });\n }\n } else {\n // Convert first row TD elements to TH\n cells.forEach((cell: HTMLElement) => {\n cell.tagName = 'th';\n });\n }\n });\n\n // Process lists - remove unicode bullets from unnumbered list items\n root.querySelectorAll('ul li').forEach((listItem: HTMLElement) => {\n // Get the text content and remove unicode bullets from the beginning\n const textContent = listItem.innerHTML;\n const cleanedContent = textContent.replace(bulletRegex, '');\n if (cleanedContent !== textContent) {\n listItem.innerHTML = cleanedContent;\n }\n });\n\n // Optionally render numbered lists as bullet lists by renaming <ol> to <ul>.\n // Doing it on the DOM (rather than rewriting `1.` markers in the Markdown)\n // can't touch numbered lines inside code blocks or literal text. Mammoth's\n // footnote/endnote list stays ordered: it's not a real list, and the footnote\n // conversion (or, with footnotes preserved, the numbered note list) relies on\n // it. Runs after the unicode-bullet strip above so formerly numbered items\n // keep their content as-is.\n if (opts.bulletLists) {\n root.querySelectorAll('ol').forEach((list: HTMLElement) => {\n if (isNoteList(list)) return;\n list.tagName = 'ul';\n list.removeAttribute('start');\n list.removeAttribute('type');\n });\n }\n\n return root.toString();\n}\n\n// Mammoth renders footnotes/endnotes as a superscript reference link plus a\n// trailing ordered list of note bodies with `↑` backlinks, not real Markdown\n// footnotes:\n//\n// <p>Text<sup><a href=\"#footnote-1\" id=\"footnote-ref-1\">[1]</a></sup>.</p>\n// <ol><li id=\"footnote-1\"><p>Body. <a href=\"#footnote-ref-1\">↑</a></p></li></ol>\n//\n// These Turndown rules emit GFM/Pandoc footnote syntax instead (`[^1]`\n// references and `[^1]:` definitions), keyed on Mammoth's stable anchor ids.\n// Endnotes use the same numbering scheme, so they share it.\nconst NOTE_REF_HREF = /^#(?:foot|end)note-(\\d+)$/;\nconst NOTE_BACKLINK_HREF = /^#(?:foot|end)note-ref-\\d+$/;\n\nfunction addFootnoteRules(service: TurndownService): void {\n // The reference: a <sup> wrapping only the link to the note.\n service.addRule('footnoteReference', {\n filter: (node) => {\n const link = node.firstElementChild;\n return (\n node.nodeName === 'SUP' &&\n node.childElementCount === 1 &&\n link?.nodeName === 'A' &&\n NOTE_REF_HREF.test(link.getAttribute('href') ?? '')\n );\n },\n replacement: (_content, node) => {\n const href = node.firstElementChild?.getAttribute('href') ?? '';\n return `[^${NOTE_REF_HREF.exec(href)?.[1]}]`;\n },\n });\n // The `↑` link back to the reference is meaningless in Markdown footnotes.\n service.addRule('footnoteBacklink', {\n filter: (node) =>\n node.nodeName === 'A' &&\n NOTE_BACKLINK_HREF.test(node.getAttribute('href') ?? ''),\n replacement: () => '',\n });\n // Each note becomes a definition. Later paragraphs of a multi-paragraph note\n // are indented four spaces, which GFM/Pandoc read as a continuation.\n service.addRule('footnoteDefinition', {\n filter: (node) =>\n node.nodeName === 'LI' &&\n NOTE_ITEM_ID.test(node.getAttribute('id') ?? ''),\n replacement: (content, node) => {\n const num = node.getAttribute('id')?.replace(/^\\D+/, '');\n const body = content\n .trim()\n .split('\\n')\n .map((line, i) =>\n i === 0 || line.trim() === '' ? line : ` ${line}`,\n )\n .join('\\n');\n return `[^${num}]: ${body}\\n\\n`;\n },\n });\n}\n\n// Allowlist link schemes on the DOM Turndown converts. A Word hyperlink (or\n// caller-supplied HTML) can target any URL, and Turndown's own `javascript:`\n// check is bypassable (e.g. a leading U+0001), so keep only http(s)/mailto,\n// fragment and relative links; drop the href of anything else so the link\n// renders as its text and can't become a script link downstream.\n//\n// This runs as a rule filter rather than a separate HTML pre-pass so it sees\n// exactly the DOM Turndown builds (no parser differential; e.g. node-html-parser\n// treats <pre> and <noscript> content as raw text). Turndown asks the rules for\n// a node before converting its children, and it's added last so it's checked\n// first, so every <a> is cleaned before any rule reads its href or an ancestor\n// is kept verbatim as HTML (keepTags, GFM tables it can't convert). The filter\n// never matches: it only cleans the node and lets the normal rules convert it.\nfunction addLinkAllowlistRule(service: TurndownService): void {\n service.addRule('linkSchemeAllowlist', {\n filter: (node) => {\n if (node.nodeName !== 'A') return false;\n const href = node.getAttribute('href');\n if (href === null) return false;\n const safe = safeHref(href);\n if (safe === null) node.removeAttribute('href');\n else if (safe !== href) node.setAttribute('href', safe);\n return false;\n },\n replacement: (content) => content,\n });\n}\n\nfunction createTurndownService(\n options: object,\n keepTags: string[],\n gfmFootnotes: boolean,\n): TurndownService {\n const service = new TurndownService({\n ...defaultTurndownOptions,\n ...options,\n });\n service.use(turndownPluginGfm.gfm);\n if (keepTags.length > 0) service.keep(keepTags);\n if (gfmFootnotes) addFootnoteRules(service);\n addLinkAllowlistRule(service);\n return service;\n}\n\n// Reusable services for the default options, keyed by keep-tags (e.g. '' or\n// 'u' for --underline) and footnote mode, so repeat conversions don't rebuild\n// Turndown and its GFM rules. `keep()` and `addRule()` mutate a service, hence\n// one per combination.\nconst turndownServices = new Map<string, TurndownService>();\n\nfunction getTurndownService(\n options: object = {},\n keepTags: string[] = [],\n gfmFootnotes = false,\n): TurndownService {\n // Caller-supplied Turndown options may hold functions (custom rules), which\n // can't be keyed reliably; build a fresh service for those.\n if (Object.keys(options).length > 0) {\n return createTurndownService(options, keepTags, gfmFootnotes);\n }\n const key = `${[...keepTags].sort().join(',')}|${gfmFootnotes}`;\n let service = turndownServices.get(key);\n if (!service) {\n service = createTurndownService({}, keepTags, gfmFootnotes);\n turndownServices.set(key, service);\n }\n return service;\n}\n\n// Convert HTML to GitHub-flavored Markdown. `keepTags` lists HTML tags to\n// preserve verbatim as inline HTML (e.g. `['u']` to keep underlines) rather\n// than let Turndown strip them to plain text. `gfmFootnotes` rewrites Mammoth's\n// footnote/endnote markup into `[^1]` footnotes (see addFootnoteRules).\nexport function htmlToMd(\n html: string,\n options: object = {},\n keepTags: string[] = [],\n gfmFootnotes = false,\n): string {\n // Turndown's DOM parser decodes entities exactly once. Don't pre-decode:\n // that would turn literal text like `<b>` into markup and truncate\n // attribute values containing `"`.\n const turndownService = getTurndownService(options, keepTags, gfmFootnotes);\n return turndownService.turndown(html).trim();\n}\n\n// Pre-compiled regex patterns for better performance\nconst nonBreakingSpacesRegex = /[\\u00A0\\u2007\\u202F\\u2060\\uFEFF]/g;\nconst smartQuotesRegex = /[\\u201C\\u201D\\u2018\\u2019]/g;\n\n// Map for non-breaking space replacements\nconst nonBreakingSpaceMap: { [key: string]: string } = {\n '\\u00A0': ' ', // Non-breaking space\n '\\u2007': ' ', // Figure space\n '\\u202F': ' ', // Narrow no-break space\n '\\u2060': '', // Word joiner (zero-width non-breaking space)\n '\\uFEFF': '', // Zero-width no-break space (BOM)\n};\n\n// Map for smart quote replacements\nconst smartQuoteMap: { [key: string]: string } = {\n '\\u201C': '\"', // Left double quotation mark\n '\\u201D': '\"', // Right double quotation mark\n '\\u2018': \"'\", // Left single quotation mark\n '\\u2019': \"'\", // Right single quotation mark\n};\n\n// Remove unicode non-breaking spaces and convert smart quotes to ASCII in a single pass\nfunction normalizeText(md: string): string {\n return md\n .replace(nonBreakingSpacesRegex, (char) => nonBreakingSpaceMap[char])\n .replace(smartQuotesRegex, (char) => smartQuoteMap[char]);\n}\n\n// Lint the Markdown and correct any issues\nfunction lint(md: string): string {\n const lintResult = markdownlint.lint({ strings: { md } });\n return applyFixes(md, lintResult['md']).trim();\n}\n\n// Format the Markdown with Prettier\nasync function prettify(md: string): Promise<string> {\n const formatted = await prettier.format(md, {\n parser: 'markdown',\n plugins: [prettierMarkdown],\n });\n return formatted.trim();\n}\n\n// Extract document properties from a .docx file\n// The text of docProps/core.xml's classification fields (cp:keywords,\n// cp:category, cp:contentStatus), with or without a namespace prefix.\nconst CORE_CLASSIFICATION_FIELDS =\n /<(?:\\w+:)?(?:keywords|category|contentStatus)\\b[^>]*>([^<]*)</g;\n\nexport async function extractDocumentProperties(\n input: string | ArrayBuffer,\n): Promise<DocumentProperties> {\n const properties: DocumentProperties = {};\n\n // A path that can't be read is the caller's problem, not a \"no properties\"\n // result, so surface it as a typed error before the lenient parsing below.\n let arrayBuffer: ArrayBuffer;\n try {\n arrayBuffer =\n typeof input === 'string' ? await readFileBytes(input) : input;\n } catch (error) {\n classifyConversionError(\n error,\n typeof input === 'string' ? input : undefined,\n );\n }\n\n try {\n const zip = await JSZip.loadAsync(arrayBuffer);\n\n // Check for encryption - encrypted files have EncryptionInfo and EncryptedPackage\n const encryptionInfo = zip.file('EncryptionInfo');\n if (encryptionInfo) {\n properties.encryption = true;\n }\n\n // Try to read core properties\n const corePropsFile = zip.file('docProps/core.xml');\n if (corePropsFile) {\n const coreXml = await corePropsFile.async('string');\n // Look for confidentiality markers in the classification fields only.\n // Free-text fields (title, subject, description, author) are skipped, so a\n // title like \"Case-sensitive search\" isn't flagged.\n for (const [, value] of coreXml.matchAll(CORE_CLASSIFICATION_FIELDS)) {\n if (/confidential|sensitive/i.test(value)) {\n properties.confidentiality = 'detected in core properties';\n break;\n }\n }\n }\n\n // Try to read custom properties\n const customPropsFile = zip.file('docProps/custom.xml');\n if (customPropsFile) {\n const customXml = await customPropsFile.async('string');\n const customXmlLower = customXml.toLowerCase();\n\n // Use regex patterns to detect sensitivity/confidentiality properties\n // We use regex instead of full XML parsing for performance and simplicity,\n // as we only need to detect the presence of specific property names, not extract values\n\n // Pattern matches common sensitivity/confidentiality property names:\n // - \"Sensitivity\" (standard Office property)\n // - \"MSIP_Label_*\" (Microsoft Information Protection labels)\n // - Any property with \"confidential\" or \"sensitive\" in the name\n // We only check for the property name attribute existence, not the full element content,\n // to avoid potential catastrophic backtracking on large/malformed XML\n const sensitivityPattern =\n /<property[^>]*\\bname=\"(?:Sensitivity|MSIP_Label_[^\"]*|[^\"]*(?:confidential|sensitive)[^\"]*)\"[^>]*>/gi;\n\n const hasSensitivityProperty = sensitivityPattern.test(customXml);\n const hasConfidentialText = customXmlLower.includes('confidential');\n const hasMSIPLabel = customXmlLower.includes('msip_label');\n\n if (hasSensitivityProperty || hasMSIPLabel) {\n properties.sensitivity = 'detected in custom properties';\n }\n\n if (hasConfidentialText) {\n properties.confidentiality = 'detected in custom properties';\n }\n }\n\n // Check for document protection\n const settingsFile = zip.file('word/settings.xml');\n if (settingsFile) {\n const settingsXml = await settingsFile.async('string');\n if (\n settingsXml.includes('<w:documentProtection') ||\n settingsXml.includes('<w:writeProtection')\n ) {\n properties.protection = true;\n }\n }\n } catch (error) {\n // If we can't extract properties, just continue without them\n // This might happen with encrypted, corrupted, or non-standard .docx files\n // We log the error in development mode but don't fail the conversion\n // `process` doesn't exist in the browser worker unless a bundler shims it\n if (\n typeof process !== 'undefined' &&\n process.env?.NODE_ENV === 'development'\n ) {\n console.warn('Failed to extract document properties:', error);\n }\n }\n\n return properties;\n}\n\n// Generate warnings based on document properties\nexport function generateWarnings(properties: DocumentProperties): string[] {\n const warnings: string[] = [];\n\n if (properties.encryption) {\n warnings.push(\n 'Warning: This document appears to be encrypted. Conversion may not include all content or may fail entirely.',\n );\n }\n\n if (properties.sensitivity) {\n warnings.push(\n `Warning: This document has sensitivity labels (${properties.sensitivity}). Please ensure you have permission to convert and share this content.`,\n );\n }\n\n if (properties.confidentiality) {\n warnings.push(\n `Warning: This document contains confidentiality markers (${properties.confidentiality}). Please verify that conversion is authorized.`,\n );\n }\n\n if (properties.protection) {\n warnings.push(\n 'Warning: This document has editing restrictions enabled. Some content may not convert properly.',\n );\n }\n\n return warnings;\n}\n\n// The input shapes mammoth accepts across environments\ntype MammothInput = { buffer: Buffer } | { arrayBuffer: ArrayBuffer };\n\n// Encrypted (password-protected) .docx files and legacy .doc files are OLE\n// Compound File Binary containers, not ZIPs. They start with this signature.\nconst CFB_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];\n\nfunction isCompoundFile(bytes: ArrayBuffer): boolean {\n const head = new Uint8Array(bytes, 0, Math.min(bytes.byteLength, 8));\n return (\n head.length === CFB_SIGNATURE.length &&\n CFB_SIGNATURE.every((byte, i) => head[i] === byte)\n );\n}\n\ninterface LoadedInput {\n bytes: ArrayBuffer;\n mammothInput: MammothInput;\n}\n\n// Read the input once and shape it for mammoth. In Node.js, mammoth's unzip\n// only accepts { path | buffer | file }; the browser build takes { arrayBuffer }.\nasync function loadInput(input: string | ArrayBuffer): Promise<LoadedInput> {\n let bytes: ArrayBuffer;\n if (typeof input === 'string') {\n validateFileExtension(input);\n bytes = await readFileBytes(input);\n } else {\n bytes = input;\n }\n\n if (isCompoundFile(bytes)) {\n throw new UnsupportedFileError(\n 'This file is password-protected or is a legacy .doc file. Please remove the password or save it as a .docx file and try again.',\n );\n }\n\n const mammothInput: MammothInput =\n typeof Buffer !== 'undefined'\n ? { buffer: Buffer.from(bytes) }\n : { arrayBuffer: bytes };\n return { bytes, mammothInput };\n}\n\n// A conversion message emitted by mammoth (e.g. dropped/unsupported content)\ninterface MammothMessage {\n type: string;\n message: string;\n}\n\n// Synthetic paragraph-style name applied to code paragraphs detected by font\n// (see tagCodeParagraphs). It's mapped to a fenced code block alongside the real\n// Word/LibreOffice preformatted styles below.\nconst DETECTED_CODE_STYLE_NAME = 'W2M Code Block';\n\n// Word and LibreOffice mark code blocks in two ways, both of which we route to a\n// single `<pre><code>`:\n// - a dedicated paragraph style — \"Preformatted Text\" (LibreOffice) or \"HTML\n// Preformatted\" (Word) — one paragraph per line; and\n// - direct monospace-font runs on otherwise-Normal paragraphs (often inside a\n// shaded 1×1 table), which tagCodeParagraphs re-labels with the synthetic\n// style name above.\n// Mapping each style to `pre > code:separator('\\n')` tells Mammoth to merge the\n// consecutive lines into one `<pre><code>` joined by newlines, which Turndown\n// emits as a single fenced block with verbatim content — no per-line paragraphs\n// and, crucially, no Markdown escaping of code punctuation (`[ ] { } * - /`). The\n// `\\n` in `separator` is a literal backslash-n in the source string; Mammoth's\n// style-map parser interprets it as a newline.\nconst CODE_BLOCK_STYLE_MAP = [\n `p[style-name='${DETECTED_CODE_STYLE_NAME}'] => pre > code:separator('\\\\n')`,\n \"p[style-name='Preformatted Text'] => pre > code:separator('\\\\n')\",\n \"p[style-name='HTML Preformatted'] => pre > code:separator('\\\\n')\",\n];\n\n// Append our style-map additions to any caller-supplied Mammoth options: the\n// code-block mappings always, plus `u => u` when underlines are being preserved\n// (Mammoth ignores underlines unless the style map maps them to a `<u>`).\n// Additions are appended, never replaced, so Mammoth's default mappings —\n// including superscript/subscript — stay intact.\nfunction withStyleMap(\n mammothOptions: MammothOptions | undefined,\n { preserveUnderline }: { preserveUnderline: boolean },\n): MammothOptions {\n const existing = mammothOptions?.styleMap ?? [];\n const additions = [...CODE_BLOCK_STYLE_MAP];\n if (preserveUnderline) additions.push('u => u');\n return { ...mammothOptions, styleMap: [...existing, ...additions] };\n}\n\n// Minimal shape of the nodes in Mammoth's document model that we walk. Mammoth\n// ships no types for this, so we narrow to just the fields we read: a node's\n// `type`, a text node's `value`, a run's `font` (the `w:rFonts` ascii name), and\n// child nodes.\ninterface MammothDocNode {\n type: string;\n value?: string;\n font?: string | null;\n children?: MammothDocNode[];\n [key: string]: unknown;\n}\n\n// Font names (lower-cased) treated as monospace/code fonts. The word-boundaried\n// `mono` matches families like \"Liberation Mono\"/\"DejaVu Sans Mono\" without\n// catching proportional fonts that merely start with those letters (e.g.\n// \"Monotype Corsiva\"); the rest are common fixed-width families.\nconst MONOSPACE_FONT_RE =\n /(^|\\s)mono(\\s|$)|monospace|consolas|courier|menlo|monaco|inconsolata/;\n\nfunction isMonospaceFont(font: string | null | undefined): boolean {\n return !!font && MONOSPACE_FONT_RE.test(font.toLowerCase());\n}\n\n// Concatenate all text under a node (runs, hyperlinks, etc.).\nfunction docNodeText(node: MammothDocNode): string {\n if (node.type === 'text') return node.value ?? '';\n return (node.children ?? []).map(docNodeText).join('');\n}\n\n// Collect every run descendant of a node (runs can be nested inside hyperlinks).\nfunction collectRuns(\n node: MammothDocNode,\n out: MammothDocNode[] = [],\n): MammothDocNode[] {\n if (node.type === 'run') out.push(node);\n (node.children ?? []).forEach((child) => collectRuns(child, out));\n return out;\n}\n\n// A paragraph reads as a code block when it has visible text and *every*\n// text-bearing run uses a monospace font. Requiring all runs (not just one) to\n// be monospace keeps ordinary prose that merely mentions a monospaced identifier\n// out of code blocks — only wholly-monospace paragraphs qualify.\nfunction isMonospaceParagraph(paragraph: MammothDocNode): boolean {\n const runs = collectRuns(paragraph).filter(\n (run) => docNodeText(run).trim().length > 0,\n );\n return runs.length > 0 && runs.every((run) => isMonospaceFont(run.font));\n}\n\n// A Mammoth `transformDocument` that recursively re-labels wholly-monospace\n// paragraphs with DETECTED_CODE_STYLE_NAME so the style map turns them into\n// fenced code blocks. Word often encodes code as monospace runs on Normal\n// paragraphs (no code paragraph style), and Mammoth discards font information\n// once it emits HTML — the document model is the only place the signal survives.\n// Headings, titles, and list items keep their structure even when set wholly in\n// a monospace font: their style (or numbering) is a stronger signal than font.\nconst STRUCTURAL_STYLE_RE = /^(heading|title|subtitle)\\b/i;\n\nfunction isStructuralParagraph(paragraph: MammothDocNode): boolean {\n const styleName = paragraph.styleName;\n return (\n Boolean(paragraph.numbering) ||\n (typeof styleName === 'string' && STRUCTURAL_STYLE_RE.test(styleName))\n );\n}\n\nfunction tagCodeParagraphs(node: MammothDocNode): MammothDocNode {\n const transformed: MammothDocNode = node.children\n ? { ...node, children: node.children.map(tagCodeParagraphs) }\n : node;\n if (\n transformed.type === 'paragraph' &&\n !isStructuralParagraph(transformed) &&\n isMonospaceParagraph(transformed)\n ) {\n return {\n ...transformed,\n styleId: 'W2MCodeBlock',\n styleName: DETECTED_CODE_STYLE_NAME,\n };\n }\n return transformed;\n}\n\n// Add the code-detection transform to any caller-supplied Mammoth options,\n// composing with (running after) an existing `transformDocument` if present.\nfunction withCodeTransform(mammothOptions: MammothOptions): MammothOptions {\n const existing = mammothOptions.transformDocument as\n ((doc: MammothDocNode) => MammothDocNode) | undefined;\n const transformDocument = existing\n ? (doc: MammothDocNode) => tagCodeParagraphs(existing(doc))\n : tagCodeParagraphs;\n return { ...mammothOptions, transformDocument };\n}\n\n// Common image content types → file extension. Word most often embeds PNG and\n// JPEG; the rest cover formats Mammoth may surface. EMF/WMF are extracted as\n// bytes for completeness but browsers can't render them (Mammoth won't transcode).\nconst CONTENT_TYPE_EXTENSIONS: { [contentType: string]: string } = {\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/tiff': 'tiff',\n 'image/bmp': 'bmp',\n 'image/webp': 'webp',\n 'image/svg+xml': 'svg',\n 'image/x-emf': 'emf',\n 'image/x-wmf': 'wmf',\n};\n\n// Pick a file extension for an extracted image. Falls back to the content\n// type's subtype when it's a clean alphanumeric token, else `bin`. Exported for\n// unit testing since only a PNG fixture exists.\n/** @internal */\nexport function extensionForContentType(contentType: string): string {\n const normalized = contentType.toLowerCase();\n const known = CONTENT_TYPE_EXTENSIONS[normalized];\n if (known) return known;\n const subtype = normalized.split('/')[1] ?? '';\n return /^[a-z0-9]+$/.test(subtype) ? subtype : 'bin';\n}\n\n// The image object Mammoth passes to a `convertImage` handler (not exported).\ntype MammothImage = Parameters<\n Parameters<typeof mammoth.images.imgElement>[0]\n>[0];\n\n// Read an image's raw bytes as a standalone Uint8Array. Mammoth types\n// `readAsArrayBuffer()` as an ArrayBuffer, but at runtime it returns JSZip's\n// `uint8array` output, which can be a Node Buffer or a view into a larger buffer\n// (e.g. an uncompressed entry sliced from the .docx itself). `new Uint8Array()`\n// covers every case: it wraps a real ArrayBuffer whole and copies any typed-array\n// view into a fresh, exactly-sized buffer. That matters for the worker, which\n// transfers each image's `.buffer` to the page (src/converter.worker.ts):\n// transferring a shared view would detach unrelated bytes.\nasync function readImageBytes(image: MammothImage): Promise<Uint8Array> {\n // No instanceof check: under Jest's vm modules (and across worker realms) a\n // Uint8Array may not be an instance of this realm's class, and the\n // constructor handles both shapes anyway.\n return new Uint8Array(await image.readAsArrayBuffer());\n}\n\n// Build a Mammoth `convertImage` handler that pulls each image out into a byte\n// array with a deterministic relative path (`imageDir/imageN.ext`) instead of\n// inlining it as a base64 data URI. The collected images are exposed on the\n// returned `images` array once conversion finishes.\nfunction createImageExtractor(imageDir: string): {\n images: ExtractedImage[];\n convertImage: unknown;\n} {\n const images: ExtractedImage[] = [];\n const convertImage = mammoth.images.imgElement(async (image) => {\n const bytes = await readImageBytes(image);\n const ext = extensionForContentType(image.contentType);\n const path = `${imageDir}/image${images.length + 1}.${ext}`;\n images.push({ path, contentType: image.contentType, bytes });\n return { src: path };\n });\n return { images, convertImage };\n}\n\n// The shared conversion pipeline: mammoth HTML -> cleaned, formatted Markdown.\n// Returns mammoth's messages so callers can surface content-loss warnings, and\n// (in extract mode) the extracted image assets.\nasync function runConversionPipeline(\n mammothInput: MammothInput,\n options: ConvertOptions,\n): Promise<{\n markdown: string;\n messages: MammothMessage[];\n images?: ExtractedImage[];\n}> {\n // Preserving underline requires cooperation at both ends of the pipeline:\n // Mammoth must be told to emit `<u>` (it drops underlines by default), and\n // Turndown must be told to keep that tag rather than flatten it to text.\n const preserveUnderline = options.underline === 'preserve';\n let mammothOptions: MammothOptions | undefined = withCodeTransform(\n withStyleMap(options.mammoth as MammothOptions | undefined, {\n preserveUnderline,\n }),\n );\n\n // Extract mode swaps Mammoth's default base64 inliner for a collector that\n // returns each image's bytes and rewrites the src to a relative path.\n let extractor:\n { images: ExtractedImage[]; convertImage: unknown } | undefined;\n if (options.images === 'extract') {\n // Drop trailing slashes so `img/` doesn't produce `img//image1.png` links\n const imageDir = (options.imageDir ?? 'images').replace(/(?<=.)\\/+$/, '');\n extractor = createImageExtractor(imageDir);\n mammothOptions = {\n ...mammothOptions,\n convertImage: extractor.convertImage,\n };\n }\n\n const mammothResult = await mammoth.convertToHtml(\n mammothInput,\n mammothOptions,\n );\n // Numbered lists stay numbered by default; flatten to bullets on request.\n const processedHtml = processHtml(mammothResult.value, {\n stripImages: options.images === 'strip',\n bulletLists: options.numberedLists === 'bullets',\n });\n // Footnotes become GFM `[^1]` by default; keep Mammoth's markup on request.\n const md = htmlToMd(\n processedHtml,\n options.turndown,\n preserveUnderline ? ['u'] : [],\n options.footnotes !== 'preserve',\n );\n const normalizedMd = normalizeText(md);\n const cleanedMd = lint(normalizedMd);\n const formattedMd = await prettify(cleanedMd);\n return {\n markdown: formattedMd,\n messages: mammothResult.messages,\n images: extractor?.images,\n };\n}\n\n// Substrings (lower-cased) that, in a thrown error's message, indicate the\n// input isn't a readable .docx (invalid/corrupt/truncated ZIP, or a missing\n// required part). Sourced from JSZip and mammoth, which throw untyped Errors.\nconst INVALID_DOCX_ERROR_PATTERNS = [\n 'end of central directory', // JSZip: invalid ZIP structure\n 'zip file', // JSZip: not a valid ZIP\n 'corrupted zip', // JSZip: corrupted ZIP file\n 'end of data reached', // JSZip: truncated file\n 'could not find file', // mammoth: missing required file in .docx\n];\n\nfunction isInvalidDocxError(message: string): boolean {\n const lower = message.toLowerCase();\n return INVALID_DOCX_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));\n}\n\n// Translate the errors thrown by the pipeline into our typed, user-facing\n// error classes. Always throws (never returns normally).\nfunction classifyConversionError(error: unknown, filePath?: string): never {\n // Re-throw our custom errors as-is\n if (error instanceof WordToMarkdownError) {\n throw error;\n }\n\n // Handle specific error types from underlying libraries\n const errorMessage = error instanceof Error ? error.message : String(error);\n const errorCode =\n error && typeof error === 'object' && 'code' in error\n ? (error as { code: string }).code\n : undefined;\n\n // File not found errors (only occur with file path inputs)\n // ENOTDIR/ELOOP mean a path component isn't a usable directory, so the\n // file can't be reached either.\n if (\n errorCode === 'ENOENT' ||\n errorCode === 'ENOTDIR' ||\n errorCode === 'ELOOP'\n ) {\n throw new FileNotFoundError(filePath);\n }\n\n // A directory was passed where a .docx file was expected\n if (errorCode === 'EISDIR') {\n throw new InvalidFileError(filePath);\n }\n\n // Permission errors (only occur with file path inputs)\n if (errorCode === 'EACCES' || errorCode === 'EPERM') {\n throw new FilePermissionError(filePath);\n }\n\n // Invalid .docx file errors (from JSZip or mammoth during file parsing).\n // JSZip/mammoth throw plain, untyped Errors with no error code, so message\n // matching is the only signal available. Matching is case-insensitive so a\n // minor wording/capitalization change upstream is less likely to slip through\n // (see INVALID_DOCX_ERROR_PATTERNS). There is a small theoretical risk of\n // matching document content, but these technical phrases are highly unlikely\n // to appear in normal content.\n if (isInvalidDocxError(errorMessage)) {\n // Note: For ArrayBuffer inputs (e.g., web uploads), filePath will be\n // undefined, so the message won't include the original filename.\n throw new InvalidFileError(filePath, error);\n }\n\n // Wrap other errors with a general conversion error\n throw new ConversionError(\n 'An error occurred while converting the document. Please ensure the file is a valid .docx file and try again.',\n error instanceof Error ? error : undefined,\n );\n}\n\n// Turn mammoth's conversion messages into user-facing warnings. Mammoth is\n// chatty and emits many cosmetic \"unrecognised style\" notices on ordinary\n// documents; we surface only messages that indicate actual content loss\n// (dropped/ignored elements or unconvertible images), de-duplicated.\nexport function extractMammothWarnings(\n messages: readonly MammothMessage[],\n): string[] {\n const seen = new Set<string>();\n const warnings: string[] = [];\n\n for (const message of messages) {\n if (message.type !== 'warning' && message.type !== 'error') {\n continue;\n }\n\n const text = message.message.toLowerCase();\n\n // Skip cosmetic style-mapping notices — they don't drop content\n if (text.includes('style')) {\n continue;\n }\n\n // Surface only messages that signal dropped or unconvertible content\n const indicatesContentLoss =\n text.includes('ignored') ||\n text.includes('unsupported') ||\n text.includes('could not') ||\n text.includes('image');\n if (!indicatesContentLoss) {\n continue;\n }\n\n const warning = `Warning: Some document content may not have converted cleanly (${message.message}).`;\n if (!seen.has(warning)) {\n seen.add(warning);\n warnings.push(warning);\n }\n }\n\n return warnings;\n}\n\n// Converts a Word document to crisp, clean Markdown with warnings\nexport async function convertWithWarnings(\n input: string | ArrayBuffer,\n options: ConvertOptions = {},\n): Promise<ConvertResult> {\n const filePath = typeof input === 'string' ? input : undefined;\n\n try {\n const loaded = await loadInput(input);\n\n // Extract document properties to check for confidentiality flags\n const properties = await extractDocumentProperties(loaded.bytes);\n const warnings = generateWarnings(properties);\n\n const { markdown, messages, images } = await runConversionPipeline(\n loaded.mammothInput,\n options,\n );\n warnings.push(...extractMammothWarnings(messages));\n\n return images ? { markdown, warnings, images } : { markdown, warnings };\n } catch (error) {\n classifyConversionError(error, filePath);\n }\n}\n\n// Converts a Word document to crisp, clean Markdown.\n// Unlike convertWithWarnings, this skips document-property extraction (and its\n// extra ZIP parse) so callers that only need the Markdown pay no overhead.\nexport default async function convert(\n input: string | ArrayBuffer,\n options: ConvertOptions = {},\n): Promise<string> {\n const filePath = typeof input === 'string' ? input : undefined;\n\n try {\n const loaded = await loadInput(input);\n\n const { markdown } = await runConversionPipeline(\n loaded.mammothInput,\n options,\n );\n return markdown;\n } catch (error) {\n classifyConversionError(error, filePath);\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "word-to-markdown",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Convert Word documents to
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Convert Word (.docx) documents to Markdown via CLI, Node.js, or browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"word",
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
"converter",
|
|
12
12
|
"mammoth",
|
|
13
13
|
"turndown",
|
|
14
|
-
"cli"
|
|
14
|
+
"cli",
|
|
15
|
+
"docx-to-markdown",
|
|
16
|
+
"word-to-markdown",
|
|
17
|
+
"microsoft-word",
|
|
18
|
+
"md",
|
|
19
|
+
"gfm"
|
|
15
20
|
],
|
|
16
21
|
"homepage": "https://word2md.com",
|
|
17
22
|
"repository": {
|
|
@@ -26,27 +31,25 @@
|
|
|
26
31
|
},
|
|
27
32
|
"devDependencies": {
|
|
28
33
|
"@astrojs/check": "^0.9.10",
|
|
29
|
-
"@astrojs/sitemap": "^3.7.
|
|
34
|
+
"@astrojs/sitemap": "^3.7.4",
|
|
30
35
|
"@eslint/js": "^10.0.1",
|
|
31
|
-
"@jest/globals": "~30.5.
|
|
36
|
+
"@jest/globals": "~30.5.2",
|
|
32
37
|
"@mixmark-io/domino": "^2.2.0",
|
|
33
|
-
"@nuasite/checks": "^0.56.
|
|
34
|
-
"@playwright/test": "^1.
|
|
38
|
+
"@nuasite/checks": "^0.56.4",
|
|
39
|
+
"@playwright/test": "^1.63.0",
|
|
35
40
|
"@tailwindcss/typography": "^0.5.20",
|
|
36
41
|
"@tailwindcss/vite": "^4.3.3",
|
|
37
42
|
"@types/jest": "~30.0",
|
|
38
43
|
"@types/node": "~26",
|
|
39
|
-
"astro": "^7.
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"eslint": "^10.8.1",
|
|
44
|
+
"astro": "^7.3.5",
|
|
45
|
+
"docx": "^9.8.1",
|
|
46
|
+
"eslint": "^10.11.0",
|
|
43
47
|
"eslint-config-prettier": "~10.1",
|
|
44
48
|
"eslint-plugin-jest": "~29.16",
|
|
45
|
-
"globals": "^17.
|
|
46
|
-
"jest": "~30.5.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"playwright": "^1.62.1",
|
|
49
|
+
"globals": "^17.12.0",
|
|
50
|
+
"jest": "~30.5.2",
|
|
51
|
+
"knip": "^6.39.0",
|
|
52
|
+
"playwright": "^1.63.0",
|
|
50
53
|
"rehype-sanitize": "^6.0.0",
|
|
51
54
|
"rehype-stringify": "^10.0.1",
|
|
52
55
|
"remark-gfm": "^4.0.1",
|
|
@@ -55,8 +58,9 @@
|
|
|
55
58
|
"tailwindcss": "^4.3.3",
|
|
56
59
|
"ts-jest": "~29.4",
|
|
57
60
|
"typescript": "^6.0.3",
|
|
58
|
-
"typescript-eslint": "^8.
|
|
59
|
-
"unified": "^11.0.5"
|
|
61
|
+
"typescript-eslint": "^8.71.0",
|
|
62
|
+
"unified": "^11.0.5",
|
|
63
|
+
"wrangler": "^4.145.0"
|
|
60
64
|
},
|
|
61
65
|
"scripts": {
|
|
62
66
|
"fix": "eslint --fix . && prettier --write .",
|
|
@@ -66,12 +70,12 @@
|
|
|
66
70
|
"build:site": "astro check --minimumSeverity error && astro build",
|
|
67
71
|
"preview": "astro preview",
|
|
68
72
|
"lint": "eslint . && prettier --check .",
|
|
73
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
69
74
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
|
70
75
|
"test:e2e": "playwright test",
|
|
71
76
|
"test:e2e:headed": "playwright test --headed",
|
|
72
77
|
"test:e2e:debug": "playwright test --debug",
|
|
73
|
-
"all": "npm run fix && npm run
|
|
74
|
-
"check-builds": "./scripts/check-builds.sh",
|
|
78
|
+
"all": "npm run fix && npm run typecheck && npm run test && npm run build",
|
|
75
79
|
"gen:specs": "node scripts/gen-fixture-specs.mjs",
|
|
76
80
|
"gen:fixtures": "npm run build:js && node scripts/build-fixtures.mjs",
|
|
77
81
|
"gen:judge": "node scripts/judge-fixtures.mjs",
|
|
@@ -83,15 +87,14 @@
|
|
|
83
87
|
"author": "Ben Balter <ben@balter.com>",
|
|
84
88
|
"license": "Apache-2.0",
|
|
85
89
|
"dependencies": {
|
|
86
|
-
"@joplin/turndown": "^4.0.
|
|
87
|
-
"@joplin/turndown-plugin-gfm": "^1.0.
|
|
90
|
+
"@joplin/turndown": "^4.0.86",
|
|
91
|
+
"@joplin/turndown-plugin-gfm": "^1.0.68",
|
|
88
92
|
"commander": "^15.0.0",
|
|
89
|
-
"jszip": "^3.10.
|
|
90
|
-
"mammoth": "^1.
|
|
93
|
+
"jszip": "^3.10.2",
|
|
94
|
+
"mammoth": "^1.13.0",
|
|
91
95
|
"markdownlint": "^0.41.1",
|
|
92
|
-
"node-html-parser": "^9.0.
|
|
93
|
-
"prettier": "~3.9"
|
|
94
|
-
"tslib": "~2.8"
|
|
96
|
+
"node-html-parser": "^9.0.4",
|
|
97
|
+
"prettier": "~3.9"
|
|
95
98
|
},
|
|
96
99
|
"volta": {
|
|
97
100
|
"node": "24.21.0"
|
|
@@ -102,6 +105,13 @@
|
|
|
102
105
|
},
|
|
103
106
|
"main": "build/main.js",
|
|
104
107
|
"types": "build/main.d.ts",
|
|
108
|
+
"exports": {
|
|
109
|
+
".": {
|
|
110
|
+
"types": "./build/main.d.ts",
|
|
111
|
+
"default": "./build/main.js"
|
|
112
|
+
},
|
|
113
|
+
"./package.json": "./package.json"
|
|
114
|
+
},
|
|
105
115
|
"files": [
|
|
106
116
|
"build/cli.js",
|
|
107
117
|
"build/cli.js.map",
|