@ryangarber/markitdown-ts 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,611 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { Command } from "@commander-js/extra-typings";
3
+ import turndownPluginGfm from "@joplin/turndown-plugin-gfm";
4
+ import TurndownService from "turndown";
5
+ import { parseHTML } from "linkedom";
6
+ import mammoth from "mammoth";
7
+ import { extractText, getDocumentProxy, getMeta } from "unpdf";
8
+ import ExcelJS from "exceljs";
9
+ import { DOMParser } from "@xmldom/xmldom";
10
+ import { unzipSync } from "fflate";
11
+ //#region package.json
12
+ var version = "0.1.4";
13
+ //#endregion
14
+ //#region src/bytes.ts
15
+ async function toUint8Array(source) {
16
+ if (source instanceof Uint8Array) return source;
17
+ if (isBlob(source)) return new Uint8Array(await source.arrayBuffer());
18
+ if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
19
+ return new Uint8Array(source);
20
+ }
21
+ function toArrayBuffer(source) {
22
+ return source.slice().buffer;
23
+ }
24
+ function decodeText(source) {
25
+ return new TextDecoder().decode(source);
26
+ }
27
+ function isBlob(value) {
28
+ return typeof Blob !== "undefined" && value instanceof Blob;
29
+ }
30
+ //#endregion
31
+ //#region src/custom-turndown.ts
32
+ var CustomTurnDown = class {
33
+ convertSoup(document) {
34
+ const service = new TurndownService({ headingStyle: "atx" });
35
+ service.use(turndownPluginGfm.gfm);
36
+ service.addRule("anchor tags", {
37
+ filter: ["a"],
38
+ replacement(content, node) {
39
+ if (!content) return "";
40
+ const prefix = content.startsWith(" ") ? " " : "";
41
+ const suffix = content.endsWith(" ") ? " " : "";
42
+ const text = content.trim().replace(/\n\n.*/g, "");
43
+ if (!text) return "";
44
+ const element = node;
45
+ const href = element.getAttribute("href");
46
+ let title = element.getAttribute("title") ?? "";
47
+ if (!href) return `${prefix}${text}${suffix}`;
48
+ try {
49
+ const url = new URL(href);
50
+ if (url.protocol !== "https:" && url.protocol !== "http:") return `${prefix}${text}${suffix}`;
51
+ } catch {
52
+ if (!href.startsWith("/") && !href.startsWith("#") && !href.startsWith("./") && !href.startsWith("../")) return `${prefix}${text}${suffix}`;
53
+ }
54
+ if (text.replace(/\\_/g, "_") === href && !title) return `<${href}>`;
55
+ if (!title) title = href;
56
+ return `${prefix}[${text}](${href}${title ? ` "${title}"` : ""})${suffix}`;
57
+ }
58
+ });
59
+ service.addRule("img tags", {
60
+ filter: ["img"],
61
+ replacement(_content, node) {
62
+ const element = node;
63
+ const alt = element.getAttribute("alt") ?? "";
64
+ let source = element.getAttribute("src") ?? "";
65
+ const title = element.getAttribute("title");
66
+ if (source.startsWith("data:")) source = `${source.split(",", 1)[0]}...`;
67
+ return `![${alt}](${source}${title ? ` "${title}"` : ""})`;
68
+ }
69
+ });
70
+ return service.turndown(document);
71
+ }
72
+ };
73
+ //#endregion
74
+ //#region src/html-document.ts
75
+ function parseHtmlDocument(html) {
76
+ return parseHTML(html).document;
77
+ }
78
+ //#endregion
79
+ //#region src/converters/bingserp.ts
80
+ const BING_SEARCH_URL = /^https:\/\/www\.bing\.com\/search\?q=/i;
81
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
82
+ var BingSerpConverter = class {
83
+ async convert(source, options = {}) {
84
+ const extension = options.file_extension?.toLowerCase() ?? "";
85
+ const url = options.url ?? "";
86
+ if (extension !== ".html" && extension !== ".htm" || !BING_SEARCH_URL.test(url)) return null;
87
+ const document = parseHtmlDocument(decodeText(source));
88
+ const query = new URL(url).searchParams.get("q") ?? "";
89
+ document.querySelectorAll(".tptt").forEach((element) => {
90
+ if (element.textContent) element.textContent += " ";
91
+ });
92
+ document.querySelectorAll(".algoSlug_icon").forEach((element) => {
93
+ element.remove();
94
+ });
95
+ const turndown = new CustomTurnDown();
96
+ const markdown = `## A Bing search for '${query}' found the following results:\n\n${[...document.querySelectorAll(".b_algo")].map((result) => {
97
+ result.querySelectorAll("a[href]").forEach((anchor) => {
98
+ const href = anchor.getAttribute("href");
99
+ if (!href) return;
100
+ try {
101
+ const encoded = new URL(href, url).searchParams.get("u");
102
+ if (encoded) anchor.setAttribute("href", decodeBingUrl(encoded));
103
+ } catch {}
104
+ });
105
+ return turndown.convertSoup(result).split(/\n+/).map((line) => line.trim()).filter(Boolean).join("\n");
106
+ }).join("\n\n")}`;
107
+ return {
108
+ title: document.title || null,
109
+ markdown,
110
+ text_content: markdown
111
+ };
112
+ }
113
+ };
114
+ function decodeBingUrl(encoded) {
115
+ const input = encoded.slice(2).replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
116
+ const bytes = [];
117
+ let bits = 0;
118
+ let bitCount = 0;
119
+ for (const character of input) {
120
+ const value = BASE64_ALPHABET.indexOf(character);
121
+ if (value < 0) return encoded;
122
+ bits = bits << 6 | value;
123
+ bitCount += 6;
124
+ if (bitCount >= 8) {
125
+ bitCount -= 8;
126
+ bytes.push(bits >> bitCount & 255);
127
+ }
128
+ }
129
+ return new TextDecoder().decode(new Uint8Array(bytes));
130
+ }
131
+ //#endregion
132
+ //#region src/converters/html.ts
133
+ var HtmlConverter = class {
134
+ async convert(source, options) {
135
+ const extension = options.file_extension?.toLowerCase() ?? "";
136
+ if (extension !== ".html" && extension !== ".htm") return null;
137
+ return this.convertHtml(decodeText(source));
138
+ }
139
+ async convertHtml(html) {
140
+ const document = parseHtmlDocument(html);
141
+ const title = document.title;
142
+ document.querySelectorAll("script, style, title").forEach((element) => {
143
+ element.remove();
144
+ });
145
+ const markdown = new CustomTurnDown().convertSoup(document);
146
+ return {
147
+ title: title || null,
148
+ markdown,
149
+ text_content: markdown
150
+ };
151
+ }
152
+ };
153
+ //#endregion
154
+ //#region src/converters/docx.ts
155
+ var DocxConverter = class extends HtmlConverter {
156
+ async convert(source, options) {
157
+ if (options.file_extension?.toLowerCase() !== ".docx") return null;
158
+ let mammothInput = { arrayBuffer: toArrayBuffer(source) };
159
+ if (typeof process !== "undefined" && process.versions !== null && process.versions.node !== null) mammothInput = { buffer: Buffer.from(source) };
160
+ const mammothOptions = {
161
+ styleMap: options.styleMap,
162
+ includeEmbeddedStyleMap: options.includeEmbeddedStyleMap,
163
+ includeDefaultStyleMap: options.includeDefaultStyleMap,
164
+ ignoreEmptyParagraphs: options.ignoreEmptyParagraphs,
165
+ idPrefix: options.idPrefix
166
+ };
167
+ if (options.transformDocument) mammothOptions.transformDocument = options.transformDocument;
168
+ const html = await mammoth.convertToHtml(mammothInput, mammothOptions);
169
+ return this.convertHtml(html.value);
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/converters/ipynb.ts
174
+ var IpynbConverter = class {
175
+ async convert(source, options = {}) {
176
+ if (options.file_extension?.toLowerCase() !== ".ipynb") return null;
177
+ const notebook = JSON.parse(decodeText(source));
178
+ const output = [];
179
+ let title = notebook.metadata?.title ?? null;
180
+ for (const cell of notebook.cells ?? []) {
181
+ const lines = cell.source ?? [];
182
+ if (cell.cell_type === "markdown") {
183
+ output.push(lines.join(""));
184
+ title ??= lines.find((line) => line.startsWith("# "))?.slice(2).trim() ?? null;
185
+ } else if (cell.cell_type === "code") output.push(`\`\`\`python\n${lines.join("")}\n\`\`\``);
186
+ else if (cell.cell_type === "raw") output.push(`\`\`\`\n${lines.join("")}\n\`\`\``);
187
+ }
188
+ const markdown = output.join("\n\n");
189
+ return {
190
+ title,
191
+ markdown,
192
+ text_content: markdown
193
+ };
194
+ }
195
+ };
196
+ //#endregion
197
+ //#region src/converters/pdf.ts
198
+ var PdfConverter = class {
199
+ async convert(source, options) {
200
+ if (options.file_extension?.toLowerCase() !== ".pdf") return null;
201
+ const pdf = await getDocumentProxy(source.slice());
202
+ try {
203
+ const [{ text }, { info }] = await Promise.all([extractText(pdf, { mergePages: true }), getMeta(pdf)]);
204
+ return {
205
+ title: typeof info.Title === "string" && info.Title.trim() ? info.Title : null,
206
+ markdown: text,
207
+ text_content: text
208
+ };
209
+ } finally {
210
+ await pdf.loadingTask.destroy();
211
+ }
212
+ }
213
+ };
214
+ //#endregion
215
+ //#region src/mime.ts
216
+ const MIME_EXTENSIONS = {
217
+ "application/atom+xml": ".atom",
218
+ "application/json": ".json",
219
+ "application/pdf": ".pdf",
220
+ "application/zip": ".zip",
221
+ "application/rss+xml": ".rss",
222
+ "application/x-pdf": ".pdf",
223
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
224
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
225
+ "application/xml": ".xml",
226
+ "text/csv": ".csv",
227
+ "text/html": ".html",
228
+ "text/markdown": ".md",
229
+ "text/plain": ".txt",
230
+ "text/xml": ".xml"
231
+ };
232
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
233
+ ".csv",
234
+ ".json",
235
+ ".log",
236
+ ".markdown",
237
+ ".md",
238
+ ".text",
239
+ ".txt"
240
+ ]);
241
+ function extensionFromMime(contentType) {
242
+ if (!contentType) return null;
243
+ const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
244
+ return MIME_EXTENSIONS[mime] ?? (mime.startsWith("text/") ? ".txt" : null);
245
+ }
246
+ function isTextExtension(extension) {
247
+ return TEXT_EXTENSIONS.has(extension.toLowerCase());
248
+ }
249
+ function extensionFromPathname(pathname) {
250
+ const filename = pathname.split("/").pop() ?? "";
251
+ const dot = filename.lastIndexOf(".");
252
+ return dot > 0 ? filename.slice(dot).toLowerCase() : null;
253
+ }
254
+ //#endregion
255
+ //#region src/converters/plain-text.ts
256
+ var PlainTextConverter = class {
257
+ async convert(source, options = {}) {
258
+ if (!isTextExtension(options.file_extension ?? "")) return null;
259
+ const content = decodeText(source);
260
+ return {
261
+ title: null,
262
+ markdown: content,
263
+ text_content: content
264
+ };
265
+ }
266
+ };
267
+ //#endregion
268
+ //#region src/converters/wikipedia.ts
269
+ const WIKIPEDIA_URL = /^https?:\/\/[a-z]{2,3}\.wikipedia\.org\//i;
270
+ var WikipediaConverter = class {
271
+ async convert(source, options = {}) {
272
+ const extension = options.file_extension?.toLowerCase() ?? "";
273
+ if (extension !== ".html" && extension !== ".htm" || !WIKIPEDIA_URL.test(options.url ?? "")) return null;
274
+ const document = parseHtmlDocument(decodeText(source));
275
+ document.querySelectorAll("script, style").forEach((element) => {
276
+ element.remove();
277
+ });
278
+ const body = document.querySelector("div#mw-content-text");
279
+ const title = document.querySelector("span.mw-page-title-main")?.textContent?.trim() || document.title || null;
280
+ const markdown = body ? `# ${title ?? "Wikipedia"}\n\n${new CustomTurnDown().convertSoup(body)}` : new CustomTurnDown().convertSoup(document);
281
+ return {
282
+ title,
283
+ markdown,
284
+ text_content: markdown
285
+ };
286
+ }
287
+ };
288
+ //#endregion
289
+ //#region src/converters/xlsx.ts
290
+ const { Workbook } = ExcelJS;
291
+ var XlsxConverter = class extends HtmlConverter {
292
+ async convert(source, options) {
293
+ if (options.file_extension?.toLowerCase() !== ".xlsx") return null;
294
+ const workbook = new Workbook();
295
+ await workbook.xlsx.load(toArrayBuffer(source));
296
+ const sections = [];
297
+ for (const worksheet of workbook.worksheets) {
298
+ const html = worksheetToHtml(worksheet);
299
+ if (!html) continue;
300
+ const result = await this.convertHtml(html);
301
+ sections.push(`## ${worksheet.name}\n${result?.markdown.trim() ?? ""}`);
302
+ }
303
+ const markdown = sections.join("\n\n");
304
+ return {
305
+ title: workbook.title || "Untitled",
306
+ markdown,
307
+ text_content: markdown
308
+ };
309
+ }
310
+ };
311
+ function worksheetToHtml(worksheet) {
312
+ const columnCount = worksheet.columnCount;
313
+ if (!columnCount || !worksheet.actualRowCount) return "";
314
+ const rows = [];
315
+ worksheet.eachRow({ includeEmpty: true }, (row) => {
316
+ const cells = [];
317
+ for (let column = 1; column <= columnCount; column++) cells.push(`<td>${escapeHtml(cellText(row.getCell(column)))}</td>`);
318
+ rows.push(`<tr>${cells.join("")}</tr>`);
319
+ });
320
+ return `<html><body><table>${rows.join("")}</table></body></html>`;
321
+ }
322
+ function cellText(cell) {
323
+ if (cell.isMerged && cell.master !== cell) return "";
324
+ return cell.text;
325
+ }
326
+ function escapeHtml(value) {
327
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
328
+ }
329
+ //#endregion
330
+ //#region src/converters/xml-rss-atom.ts
331
+ var RSSConverter = class {
332
+ async convert(source, options = {}) {
333
+ const extension = options.file_extension?.toLowerCase() ?? "";
334
+ if (![
335
+ ".xml",
336
+ ".rss",
337
+ ".atom"
338
+ ].includes(extension)) return null;
339
+ const document = new DOMParser().parseFromString(decodeText(source), "text/xml");
340
+ if (document.getElementsByTagName("rss").length > 0) return this.parseRss(document);
341
+ if (document.getElementsByTagName("feed")[0]?.getElementsByTagName("entry").length) return this.parseAtom(document);
342
+ return null;
343
+ }
344
+ parseAtom(document) {
345
+ const root = document.getElementsByTagName("feed")[0];
346
+ if (!root) return null;
347
+ const title = getText(root, "title");
348
+ const subtitle = getText(root, "subtitle");
349
+ const sections = [`# ${title ?? "Feed"}`];
350
+ if (subtitle) sections.push(subtitle);
351
+ for (const entry of [...root.getElementsByTagName("entry")]) {
352
+ const entryTitle = getText(entry, "title");
353
+ const updated = getText(entry, "updated");
354
+ const summary = getText(entry, "summary");
355
+ const content = getText(entry, "content");
356
+ const lines = [];
357
+ if (entryTitle) lines.push(`## ${entryTitle}`);
358
+ if (updated) lines.push(`Updated on: ${updated}`);
359
+ if (summary) lines.push(parseContent(summary));
360
+ if (content) lines.push(parseContent(content));
361
+ sections.push(lines.join("\n"));
362
+ }
363
+ const markdown = sections.join("\n\n");
364
+ return {
365
+ title,
366
+ markdown,
367
+ text_content: markdown
368
+ };
369
+ }
370
+ parseRss(document) {
371
+ const channel = document.getElementsByTagName("rss")[0]?.getElementsByTagName("channel")[0];
372
+ if (!channel) return null;
373
+ const title = getText(channel, "title");
374
+ const description = getText(channel, "description");
375
+ const sections = title ? [`# ${title}`] : [];
376
+ if (description) sections.push(description);
377
+ for (const item of [...channel.getElementsByTagName("item")]) {
378
+ const itemTitle = getText(item, "title");
379
+ const published = getText(item, "pubDate");
380
+ const itemDescription = getText(item, "description");
381
+ const content = getText(item, "content:encoded");
382
+ const lines = [];
383
+ if (itemTitle) lines.push(`## ${itemTitle}`);
384
+ if (published) lines.push(`Published on: ${published}`);
385
+ if (itemDescription) lines.push(parseContent(itemDescription));
386
+ if (content) lines.push(parseContent(content));
387
+ sections.push(lines.join("\n"));
388
+ }
389
+ const markdown = sections.join("\n\n");
390
+ return {
391
+ title,
392
+ markdown,
393
+ text_content: markdown
394
+ };
395
+ }
396
+ };
397
+ function parseContent(content) {
398
+ return new CustomTurnDown().convertSoup(parseHtmlDocument(content).documentElement);
399
+ }
400
+ function getText(element, tagName) {
401
+ return element.getElementsByTagName(tagName)[0]?.textContent?.trim() || null;
402
+ }
403
+ //#endregion
404
+ //#region src/converters/youtube.ts
405
+ var YouTubeConverter = class {
406
+ async convert(source, options = {}) {
407
+ const extension = options.file_extension?.toLowerCase() ?? "";
408
+ if (extension !== ".html" && extension !== ".htm" || !options.url?.startsWith("https://www.youtube.com/watch?")) return null;
409
+ const document = parseHtmlDocument(decodeText(source));
410
+ const metadata = { title: document.title };
411
+ document.querySelectorAll("meta").forEach((meta) => {
412
+ const content = meta.getAttribute("content");
413
+ const key = meta.getAttribute("itemprop") ?? meta.getAttribute("property") ?? meta.getAttribute("name");
414
+ if (key && content) metadata[key] = content;
415
+ });
416
+ try {
417
+ for (const script of document.querySelectorAll("script")) {
418
+ const content = script.textContent ?? "";
419
+ if (!content.includes("ytInitialData")) continue;
420
+ const start = content.indexOf("{");
421
+ const end = content.lastIndexOf("}");
422
+ if (start >= 0 && end > start) {
423
+ const description = findKey(JSON.parse(content.slice(start, end + 1)), "attributedDescriptionBodyText");
424
+ if (isRecord(description) && typeof description.content === "string") metadata.description = description.content;
425
+ }
426
+ break;
427
+ }
428
+ } catch {}
429
+ const title = first(metadata, [
430
+ "title",
431
+ "og:title",
432
+ "name"
433
+ ]) || document.title || null;
434
+ const lines = ["# YouTube"];
435
+ if (title) lines.push(`## ${title}`);
436
+ const stats = [];
437
+ const views = first(metadata, ["interactionCount"]);
438
+ const keywords = first(metadata, ["keywords"]);
439
+ const runtime = first(metadata, ["duration"]);
440
+ if (views) stats.push(`- **Views:** ${views}`);
441
+ if (keywords) stats.push(`- **Keywords:** ${keywords}`);
442
+ if (runtime) stats.push(`- **Runtime:** ${runtime}`);
443
+ if (stats.length > 0) lines.push(`### Video Metadata\n${stats.join("\n")}`);
444
+ const description = first(metadata, ["description", "og:description"]);
445
+ if (description) lines.push(`### Description\n${description}`);
446
+ const markdown = lines.join("\n\n");
447
+ return {
448
+ title,
449
+ markdown,
450
+ text_content: markdown
451
+ };
452
+ }
453
+ };
454
+ function first(metadata, keys) {
455
+ for (const key of keys) if (metadata[key]) return metadata[key];
456
+ return null;
457
+ }
458
+ function findKey(value, key) {
459
+ if (Array.isArray(value)) for (const item of value) {
460
+ const match = findKey(item, key);
461
+ if (match !== void 0) return match;
462
+ }
463
+ else if (isRecord(value)) {
464
+ if (key in value) return value[key];
465
+ for (const item of Object.values(value)) {
466
+ const match = findKey(item, key);
467
+ if (match !== void 0) return match;
468
+ }
469
+ }
470
+ }
471
+ function isRecord(value) {
472
+ return typeof value === "object" && value !== null;
473
+ }
474
+ //#endregion
475
+ //#region src/converters/zip.ts
476
+ var ZipConverter = class {
477
+ async convert(source, options = {}) {
478
+ if (options.file_extension?.toLowerCase() !== ".zip") return null;
479
+ const files = unzipSync(source, { filter: (file) => !file.name.endsWith("/") });
480
+ const converters = options._parent_converters ?? [];
481
+ const sections = [`Content from the zip file \`${options.url ?? "archive.zip"}\`:`];
482
+ for (const [name, bytes] of Object.entries(files)) {
483
+ const result = await convertEntry(bytes, name, converters, options);
484
+ if (result) sections.push(`## File: ${name}\n\n${result.markdown}`);
485
+ }
486
+ const markdown = sections.join("\n\n");
487
+ return {
488
+ title: null,
489
+ markdown,
490
+ text_content: markdown
491
+ };
492
+ }
493
+ };
494
+ async function convertEntry(source, name, converters, options) {
495
+ const extension = extensionFromPathname(name);
496
+ if (!extension) return null;
497
+ const entryOptions = {
498
+ ...options,
499
+ file_extension: extension,
500
+ url: void 0,
501
+ _parent_converters: converters
502
+ };
503
+ for (const converter of converters) try {
504
+ const result = await converter.convert(source, entryOptions);
505
+ if (result) return result;
506
+ } catch {}
507
+ return null;
508
+ }
509
+ //#endregion
510
+ //#region src/markitdown.ts
511
+ var MarkItDown = class {
512
+ constructor() {
513
+ this.converters = [];
514
+ this.registerConverter(new PlainTextConverter());
515
+ this.registerConverter(new HtmlConverter());
516
+ this.registerConverter(new ZipConverter());
517
+ this.registerConverter(new RSSConverter());
518
+ this.registerConverter(new WikipediaConverter());
519
+ this.registerConverter(new YouTubeConverter());
520
+ this.registerConverter(new BingSerpConverter());
521
+ this.registerConverter(new DocxConverter());
522
+ this.registerConverter(new XlsxConverter());
523
+ this.registerConverter(new PdfConverter());
524
+ this.registerConverter(new IpynbConverter());
525
+ }
526
+ /** Convert a URL, response, blob, array buffer, data view, or typed array. */
527
+ async convert(source, options = {}) {
528
+ if (typeof source === "string" || source instanceof URL) return this.convertUrl(source.toString(), options);
529
+ if (isResponse(source)) return this.convertResponse(source, options);
530
+ const extensions = /* @__PURE__ */ new Set();
531
+ this.addExtension(extensions, options.file_extension?.replace(/^\./, ""));
532
+ if (isBlob(source)) {
533
+ this.addExtension(extensions, extensionFromMime(source.type));
534
+ if ("name" in source && typeof source.name === "string") this.addExtension(extensions, extensionFromPathname(source.name));
535
+ }
536
+ if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` when converting bytes.");
537
+ return this.convertBytes(await toUint8Array(source), extensions, options);
538
+ }
539
+ /**
540
+ * @deprecated Pass the byte source directly to `convert`.
541
+ */
542
+ async convertBuffer(source, options) {
543
+ return this.convert(source, options);
544
+ }
545
+ async convertUrl(source, { fetch: fetchImplementation = globalThis.fetch, ...options }) {
546
+ const url = new URL(source);
547
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Unsupported URL protocol: ${url.protocol}`);
548
+ if (!fetchImplementation) throw new Error("No fetch implementation is available.");
549
+ const response = await fetchImplementation(url);
550
+ if (!response.ok) throw new Error(`Failed to fetch URL: ${url}, status: ${response.status}`);
551
+ return this.convertResponse(response, {
552
+ ...options,
553
+ url: url.toString()
554
+ });
555
+ }
556
+ async convertResponse(response, options) {
557
+ const extensions = /* @__PURE__ */ new Set();
558
+ this.addExtension(extensions, options.file_extension);
559
+ this.addExtension(extensions, extensionFromMime(response.headers.get("content-type")));
560
+ const filename = (response.headers.get("content-disposition") ?? "").match(/filename\*?=(?:UTF-8''|["']?)([^;"']+)/i)?.[1];
561
+ if (filename) this.addExtension(extensions, extensionFromPathname(decodeURIComponent(filename.trim())));
562
+ const responseUrl = options.url || response.url;
563
+ if (responseUrl) this.addExtension(extensions, extensionFromPathname(new URL(responseUrl).pathname));
564
+ if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` or a Content-Type header.");
565
+ return this.convertBytes(new Uint8Array(await response.arrayBuffer()), extensions, {
566
+ ...options,
567
+ url: responseUrl
568
+ });
569
+ }
570
+ async convertBytes(source, extensions, options) {
571
+ let lastError;
572
+ for (const extension of extensions) for (const converter of this.converters) try {
573
+ const result = await converter.convert(source, {
574
+ ...options,
575
+ file_extension: extension,
576
+ _parent_converters: this.converters
577
+ });
578
+ if (result) {
579
+ result.markdown = result.markdown.replace(/\r\n|\r|\n/g, "\n").trim().replace(/\n{3,}/g, "\n\n");
580
+ result.text_content = result.markdown;
581
+ return result;
582
+ }
583
+ } catch (error) {
584
+ if (options.debug) console.error("Converter threw error:", error);
585
+ lastError = error;
586
+ }
587
+ const formats = [...extensions].join(", ");
588
+ if (lastError) throw new Error(`Could not convert ${formats} to Markdown: ${String(lastError)}`);
589
+ throw new Error(`Unsupported file type: ${formats}.`);
590
+ }
591
+ addExtension(extensions, extension) {
592
+ if (!extension) return;
593
+ extensions.add(extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`);
594
+ }
595
+ registerConverter(converter) {
596
+ this.converters.unshift(converter);
597
+ }
598
+ };
599
+ function isResponse(value) {
600
+ return typeof Response !== "undefined" && value instanceof Response;
601
+ }
602
+ //#endregion
603
+ //#region src/cli.ts
604
+ new Command().name("markitdown").version(version).argument("<file>", "The file to convert to Markdown").action(async (file) => {
605
+ const extension = file.split(".").pop()?.toLowerCase();
606
+ const bytes = await readFile(file);
607
+ const result = await new MarkItDown().convert(new Uint8Array(bytes), { file_extension: extension });
608
+ console.log(result);
609
+ }).parse();
610
+ //#endregion
611
+ export {};
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ let mammoth = require("mammoth");
30
30
  mammoth = __toESM(mammoth, 1);
31
31
  let unpdf = require("unpdf");
32
32
  let exceljs = require("exceljs");
33
+ exceljs = __toESM(exceljs, 1);
33
34
  let _xmldom_xmldom = require("@xmldom/xmldom");
34
35
  let fflate = require("fflate");
35
36
  //#region src/bytes.ts
@@ -159,12 +160,13 @@ var HtmlConverter = class {
159
160
  }
160
161
  async convertHtml(html) {
161
162
  const document = parseHtmlDocument(html);
162
- document.querySelectorAll("script, style").forEach((element) => {
163
+ const title = document.title;
164
+ document.querySelectorAll("script, style, title").forEach((element) => {
163
165
  element.remove();
164
166
  });
165
- const markdown = new CustomTurnDown().convertSoup(document.body ?? document);
167
+ const markdown = new CustomTurnDown().convertSoup(document);
166
168
  return {
167
- title: document.title || null,
169
+ title: title || null,
168
170
  markdown,
169
171
  text_content: markdown
170
172
  };
@@ -175,14 +177,17 @@ var HtmlConverter = class {
175
177
  var DocxConverter = class extends HtmlConverter {
176
178
  async convert(source, options) {
177
179
  if (options.file_extension?.toLowerCase() !== ".docx") return null;
178
- const html = await mammoth.default.convertToHtml({ arrayBuffer: toArrayBuffer(source) }, {
180
+ let mammothInput = { arrayBuffer: toArrayBuffer(source) };
181
+ if (typeof process !== "undefined" && process.versions !== null && process.versions.node !== null) mammothInput = { buffer: Buffer.from(source) };
182
+ const mammothOptions = {
179
183
  styleMap: options.styleMap,
180
184
  includeEmbeddedStyleMap: options.includeEmbeddedStyleMap,
181
185
  includeDefaultStyleMap: options.includeDefaultStyleMap,
182
186
  ignoreEmptyParagraphs: options.ignoreEmptyParagraphs,
183
- idPrefix: options.idPrefix,
184
- transformDocument: options.transformDocument
185
- });
187
+ idPrefix: options.idPrefix
188
+ };
189
+ if (options.transformDocument) mammothOptions.transformDocument = options.transformDocument;
190
+ const html = await mammoth.default.convertToHtml(mammothInput, mammothOptions);
186
191
  return this.convertHtml(html.value);
187
192
  }
188
193
  };
@@ -304,10 +309,11 @@ var WikipediaConverter = class {
304
309
  };
305
310
  //#endregion
306
311
  //#region src/converters/xlsx.ts
312
+ const { Workbook } = exceljs.default;
307
313
  var XlsxConverter = class extends HtmlConverter {
308
314
  async convert(source, options) {
309
315
  if (options.file_extension?.toLowerCase() !== ".xlsx") return null;
310
- const workbook = new exceljs.Workbook();
316
+ const workbook = new Workbook();
311
317
  await workbook.xlsx.load(toArrayBuffer(source));
312
318
  const sections = [];
313
319
  for (const worksheet of workbook.worksheets) {
@@ -544,7 +550,7 @@ var MarkItDown = class {
544
550
  if (typeof source === "string" || source instanceof URL) return this.convertUrl(source.toString(), options);
545
551
  if (isResponse(source)) return this.convertResponse(source, options);
546
552
  const extensions = /* @__PURE__ */ new Set();
547
- this.addExtension(extensions, options.file_extension);
553
+ this.addExtension(extensions, options.file_extension?.replace(/^\./, ""));
548
554
  if (isBlob(source)) {
549
555
  this.addExtension(extensions, extensionFromMime(source.type));
550
556
  if ("name" in source && typeof source.name === "string") this.addExtension(extensions, extensionFromPathname(source.name));
@@ -597,6 +603,7 @@ var MarkItDown = class {
597
603
  return result;
598
604
  }
599
605
  } catch (error) {
606
+ if (options.debug) console.error("Converter threw error:", error);
600
607
  lastError = error;
601
608
  }
602
609
  const formats = [...extensions].join(", ");