@ryangarber/markitdown-ts 0.1.2 → 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 {};