word-to-markdown 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
- await writeFile(options.output, result.markdown);
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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","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 .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 await writeFile(options.output, result.markdown);\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 process.exit(1);\n }\n });\n\nawait program.parseAsync();\n"]}
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
- // Validates that a file path is safe to use and returns the resolved path (Node.js only)
71
- function validateFilePath(filePath) {
72
- // Resolve to an absolute path first. path.resolve normalizes any `..`
73
- // segments, so we deliberately do NOT reject paths that merely contain `..`:
74
- // a relative path like `../report.docx`, or a filename like `notes..docx`, is
75
- // legitimate CLI/library usage. (There is no sandbox to escape here — a local
76
- // caller already has full filesystem access.)
77
- // Note: the path module is Node.js-only, but this function is only called in
78
- // Node.js contexts (CLI, direct API use with file paths).
79
- const resolvedPath = path.resolve(filePath);
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
- return root.toString();
182
- }
183
- // Reusable TurndownService instance to avoid recreating it for each conversion
184
- let turndownServiceInstance = null;
185
- function getTurndownService(options = {}, keepTags = []) {
186
- // Create a new instance if options or keep-tags are provided; otherwise reuse
187
- // the singleton. `keep()` mutates the instance, so it must never touch the
188
- // shared singleton — a fresh service is required whenever keepTags is set.
189
- if (Object.keys(options).length > 0 || keepTags.length > 0) {
190
- const service = new TurndownService({
191
- ...defaultTurndownOptions,
192
- ...options,
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 turndownServiceInstance;
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
- export function htmlToMd(html, options = {}, keepTags = []) {
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 `&#60;b&#62;` into markup and truncate
212
333
  // attribute values containing `&quot;`.
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 keywords that might indicate sensitivity/confidentiality
337
- if (coreXml.toLowerCase().includes('confidential') ||
338
- coreXml.toLowerCase().includes('sensitive')) {
339
- properties.confidentiality = 'detected in core properties';
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
- // Validate the file path to prevent path traversal attacks
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' && isMonospaceParagraph(transformed)) {
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
- // Decode a base64 string to bytes in both Node (Buffer) and the browser (atob).
562
- // Mammoth's `image.read('base64')` is the one encoding available in every build.
563
- function base64ToBytes(b64) {
564
- if (typeof Buffer !== 'undefined') {
565
- return new Uint8Array(Buffer.from(b64, 'base64'));
566
- }
567
- const binary = atob(b64);
568
- const bytes = new Uint8Array(binary.length);
569
- for (let i = 0; i < binary.length; i++)
570
- bytes[i] = binary.charCodeAt(i);
571
- return bytes;
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 = base64ToBytes(await image.read('base64'));
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
- const md = htmlToMd(processedHtml, options.turndown, preserveUnderline ? ['u'] : []);
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
- // Footnotes stay as GFM `[^1]` by default; skip the rewrite on request.
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":"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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 `![](imageDir/imageN.ext)` 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 (`![](imageDir/imageN.ext)`) 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 `&#60;b&#62;` into markup and truncate\n // attribute values containing `&quot;`.\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 `![](imageDir/imageN.ext)` 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 (`![](imageDir/imageN.ext)`) 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 `&#60;b&#62;` into markup and truncate\n // attribute values containing `&quot;`.\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,6 +1,6 @@
1
1
  {
2
2
  "name": "word-to-markdown",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Convert Word documents to beautiful Markdown.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -37,14 +37,12 @@
37
37
  "@types/jest": "~30.0",
38
38
  "@types/node": "~26",
39
39
  "astro": "^7.2.2",
40
- "clipboard": "^2.0.11",
41
40
  "docx": "^9.7.1",
42
41
  "eslint": "^10.8.1",
43
42
  "eslint-config-prettier": "~10.1",
44
43
  "eslint-plugin-jest": "~29.16",
45
44
  "globals": "^17.11.0",
46
45
  "jest": "~30.5.0",
47
- "jest-util": "~30.5.0",
48
46
  "knip": "^6.38.0",
49
47
  "playwright": "^1.62.1",
50
48
  "rehype-sanitize": "^6.0.0",
@@ -56,7 +54,8 @@
56
54
  "ts-jest": "~29.4",
57
55
  "typescript": "^6.0.3",
58
56
  "typescript-eslint": "^8.67.0",
59
- "unified": "^11.0.5"
57
+ "unified": "^11.0.5",
58
+ "wrangler": "^4.142.0"
60
59
  },
61
60
  "scripts": {
62
61
  "fix": "eslint --fix . && prettier --write .",
@@ -66,12 +65,12 @@
66
65
  "build:site": "astro check --minimumSeverity error && astro build",
67
66
  "preview": "astro preview",
68
67
  "lint": "eslint . && prettier --check .",
68
+ "typecheck": "tsc -p tsconfig.test.json",
69
69
  "test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
70
70
  "test:e2e": "playwright test",
71
71
  "test:e2e:headed": "playwright test --headed",
72
72
  "test:e2e:debug": "playwright test --debug",
73
- "all": "npm run fix && npm run test && npm run build && npm run check-builds",
74
- "check-builds": "./scripts/check-builds.sh",
73
+ "all": "npm run fix && npm run typecheck && npm run test && npm run build",
75
74
  "gen:specs": "node scripts/gen-fixture-specs.mjs",
76
75
  "gen:fixtures": "npm run build:js && node scripts/build-fixtures.mjs",
77
76
  "gen:judge": "node scripts/judge-fixtures.mjs",
@@ -90,8 +89,7 @@
90
89
  "mammoth": "^1.12.1",
91
90
  "markdownlint": "^0.41.1",
92
91
  "node-html-parser": "^9.0.1",
93
- "prettier": "~3.9",
94
- "tslib": "~2.8"
92
+ "prettier": "~3.9"
95
93
  },
96
94
  "volta": {
97
95
  "node": "24.21.0"
@@ -102,6 +100,13 @@
102
100
  },
103
101
  "main": "build/main.js",
104
102
  "types": "build/main.d.ts",
103
+ "exports": {
104
+ ".": {
105
+ "types": "./build/main.d.ts",
106
+ "default": "./build/main.js"
107
+ },
108
+ "./package.json": "./package.json"
109
+ },
105
110
  "files": [
106
111
  "build/cli.js",
107
112
  "build/cli.js.map",