@ryangarber/markitdown-ts 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vaibhav Raj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # markitdown-ts
2
+
3
+ `markitdown-ts` converts web-native document data to Markdown without filesystem, process, stream, or other runtime-specific dependencies. It is designed for browsers, workers, edge functions, and all other modern JavaScript environments.
4
+
5
+ ## Supported formats
6
+
7
+ - Plain text, Markdown, CSV, and JSON
8
+ - HTML
9
+ - XML, RSS, and Atom feeds
10
+ - Jupyter notebooks (`.ipynb`)
11
+ - Word documents (`.docx`)
12
+ - Excel workbooks (`.xlsx`)
13
+ - Wikipedia, YouTube metadata, and Bing result pages supplied as HTML
14
+
15
+ PDF, archive, audio, image metadata, local-path, and YouTube transcript features from the original project are intentionally not included because their implementations depended on runtime-specific modules, binaries, streams, or filesystem access.
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install @ryangarber/markitdown-ts
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### `Uint8Array` (preferred)
26
+
27
+ ```ts
28
+ import { MarkItDown } from "markitdown-ts";
29
+
30
+ const bytes = new TextEncoder().encode("Hello from the web");
31
+ const result = await new MarkItDown().convert(bytes, {
32
+ file_extension: ".txt"
33
+ });
34
+
35
+ console.log(result?.markdown);
36
+ ```
37
+
38
+ Any `Uint8Array` subclass is accepted as well. This keeps byte data from other modern JavaScript runtimes interoperable without requiring runtime-specific types.
39
+
40
+ ### Other in-memory sources
41
+
42
+ The same `convert` method accepts:
43
+
44
+ - `ArrayBuffer` and `SharedArrayBuffer`
45
+ - Any typed array or `DataView`
46
+ - `Blob` and `File`
47
+ - `Response`
48
+
49
+ A `file_extension` is required when the source does not carry a recognized MIME type or filename.
50
+
51
+ ```ts
52
+ const file = new File([docxBytes], "document.docx");
53
+ const result = await new MarkItDown().convert(file);
54
+ ```
55
+
56
+ ### URLs
57
+
58
+ HTTP and HTTPS URLs are fetched with the global `fetch`. Pass a custom implementation when needed.
59
+
60
+ ```ts
61
+ const result = await new MarkItDown().convert("https://example.com/article.html");
62
+ ```
63
+
64
+ Browser requests remain subject to the remote server's CORS policy.
65
+
66
+ ### Responses
67
+
68
+ ```ts
69
+ const response = await fetch("https://example.com/feed.xml");
70
+ const result = await new MarkItDown().convert(response);
71
+ ```
72
+
73
+ ## API
74
+
75
+ ```ts
76
+ type ByteSource = Uint8Array | ArrayBufferLike | ArrayBufferView | Blob;
77
+ type ConversionSource = ByteSource | Response | URL | string;
78
+
79
+ class MarkItDown {
80
+ convert(source: ConversionSource, options?: ConverterOptions): Promise<ConverterResult>;
81
+
82
+ /** @deprecated Pass the source directly to convert(). */
83
+ convertBuffer(
84
+ source: ByteSource,
85
+ options: ConverterOptions & { file_extension: string }
86
+ ): Promise<ConverterResult>;
87
+ }
88
+ ```
89
+
90
+ `convertBuffer` remains as a compatibility alias, but `convert(bytes, options)` is preferred.
91
+
92
+ ## Development
93
+
94
+ ```sh
95
+ pnpm install
96
+ pnpm check
97
+ pnpm test --run
98
+ pnpm build
99
+ ```
100
+
101
+ Biome handles both formatting and linting. `tsup` produces ESM, CommonJS, source maps, and declarations in `dist`.
102
+
103
+ ## License
104
+
105
+ MIT License © 2024 Vaibhav Raj
package/dist/index.cjs ADDED
@@ -0,0 +1,543 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _joplin_turndown_plugin_gfm = require("@joplin/turndown-plugin-gfm");
25
+ _joplin_turndown_plugin_gfm = __toESM(_joplin_turndown_plugin_gfm, 1);
26
+ let turndown = require("turndown");
27
+ turndown = __toESM(turndown, 1);
28
+ let linkedom = require("linkedom");
29
+ let mammoth = require("mammoth");
30
+ mammoth = __toESM(mammoth, 1);
31
+ let xlsx = require("xlsx");
32
+ xlsx = __toESM(xlsx, 1);
33
+ let _xmldom_xmldom = require("@xmldom/xmldom");
34
+ //#region src/bytes.ts
35
+ async function toUint8Array(source) {
36
+ if (source instanceof Uint8Array) return source;
37
+ if (isBlob(source)) return new Uint8Array(await source.arrayBuffer());
38
+ if (ArrayBuffer.isView(source)) return new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
39
+ return new Uint8Array(source);
40
+ }
41
+ function toArrayBuffer(source) {
42
+ return source.slice().buffer;
43
+ }
44
+ function decodeText(source) {
45
+ return new TextDecoder().decode(source);
46
+ }
47
+ function isBlob(value) {
48
+ return typeof Blob !== "undefined" && value instanceof Blob;
49
+ }
50
+ //#endregion
51
+ //#region src/custom-turndown.ts
52
+ var CustomTurnDown = class {
53
+ convertSoup(document) {
54
+ const service = new turndown.default({ headingStyle: "atx" });
55
+ service.use(_joplin_turndown_plugin_gfm.default.gfm);
56
+ service.addRule("anchor tags", {
57
+ filter: ["a"],
58
+ replacement(content, node) {
59
+ if (!content) return "";
60
+ const prefix = content.startsWith(" ") ? " " : "";
61
+ const suffix = content.endsWith(" ") ? " " : "";
62
+ const text = content.trim().replace(/\n\n.*/g, "");
63
+ if (!text) return "";
64
+ const element = node;
65
+ const href = element.getAttribute("href");
66
+ let title = element.getAttribute("title") ?? "";
67
+ if (!href) return `${prefix}${text}${suffix}`;
68
+ try {
69
+ const url = new URL(href);
70
+ if (url.protocol !== "https:" && url.protocol !== "http:") return `${prefix}${text}${suffix}`;
71
+ } catch {
72
+ if (!href.startsWith("/") && !href.startsWith("#") && !href.startsWith("./") && !href.startsWith("../")) return `${prefix}${text}${suffix}`;
73
+ }
74
+ if (text.replace(/\\_/g, "_") === href && !title) return `<${href}>`;
75
+ if (!title) title = href;
76
+ return `${prefix}[${text}](${href}${title ? ` "${title}"` : ""})${suffix}`;
77
+ }
78
+ });
79
+ service.addRule("img tags", {
80
+ filter: ["img"],
81
+ replacement(_content, node) {
82
+ const element = node;
83
+ const alt = element.getAttribute("alt") ?? "";
84
+ let source = element.getAttribute("src") ?? "";
85
+ const title = element.getAttribute("title");
86
+ if (source.startsWith("data:")) source = `${source.split(",", 1)[0]}...`;
87
+ return `![${alt}](${source}${title ? ` "${title}"` : ""})`;
88
+ }
89
+ });
90
+ return service.turndown(document);
91
+ }
92
+ };
93
+ //#endregion
94
+ //#region src/html-document.ts
95
+ function parseHtmlDocument(html) {
96
+ return (0, linkedom.parseHTML)(html).document;
97
+ }
98
+ //#endregion
99
+ //#region src/converters/bingserp.ts
100
+ const BING_SEARCH_URL = /^https:\/\/www\.bing\.com\/search\?q=/i;
101
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
102
+ var BingSerpConverter = class {
103
+ async convert(source, options = {}) {
104
+ const extension = options.file_extension?.toLowerCase() ?? "";
105
+ const url = options.url ?? "";
106
+ if (extension !== ".html" && extension !== ".htm" || !BING_SEARCH_URL.test(url)) return null;
107
+ const document = parseHtmlDocument(decodeText(source));
108
+ const query = new URL(url).searchParams.get("q") ?? "";
109
+ document.querySelectorAll(".tptt").forEach((element) => {
110
+ if (element.textContent) element.textContent += " ";
111
+ });
112
+ document.querySelectorAll(".algoSlug_icon").forEach((element) => {
113
+ element.remove();
114
+ });
115
+ const turndown = new CustomTurnDown();
116
+ const markdown = `## A Bing search for '${query}' found the following results:\n\n${[...document.querySelectorAll(".b_algo")].map((result) => {
117
+ result.querySelectorAll("a[href]").forEach((anchor) => {
118
+ const href = anchor.getAttribute("href");
119
+ if (!href) return;
120
+ try {
121
+ const encoded = new URL(href, url).searchParams.get("u");
122
+ if (encoded) anchor.setAttribute("href", decodeBingUrl(encoded));
123
+ } catch {}
124
+ });
125
+ return turndown.convertSoup(result).split(/\n+/).map((line) => line.trim()).filter(Boolean).join("\n");
126
+ }).join("\n\n")}`;
127
+ return {
128
+ title: document.title || null,
129
+ markdown,
130
+ text_content: markdown
131
+ };
132
+ }
133
+ };
134
+ function decodeBingUrl(encoded) {
135
+ const input = encoded.slice(2).replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
136
+ const bytes = [];
137
+ let bits = 0;
138
+ let bitCount = 0;
139
+ for (const character of input) {
140
+ const value = BASE64_ALPHABET.indexOf(character);
141
+ if (value < 0) return encoded;
142
+ bits = bits << 6 | value;
143
+ bitCount += 6;
144
+ if (bitCount >= 8) {
145
+ bitCount -= 8;
146
+ bytes.push(bits >> bitCount & 255);
147
+ }
148
+ }
149
+ return new TextDecoder().decode(new Uint8Array(bytes));
150
+ }
151
+ //#endregion
152
+ //#region src/converters/html.ts
153
+ var HtmlConverter = class {
154
+ async convert(source, options) {
155
+ const extension = options.file_extension?.toLowerCase() ?? "";
156
+ if (extension !== ".html" && extension !== ".htm") return null;
157
+ return this.convertHtml(decodeText(source));
158
+ }
159
+ async convertHtml(html) {
160
+ const document = parseHtmlDocument(html);
161
+ document.querySelectorAll("script, style").forEach((element) => {
162
+ element.remove();
163
+ });
164
+ const markdown = new CustomTurnDown().convertSoup(document.body ?? document);
165
+ return {
166
+ title: document.title || null,
167
+ markdown,
168
+ text_content: markdown
169
+ };
170
+ }
171
+ };
172
+ //#endregion
173
+ //#region src/converters/docx.ts
174
+ var DocxConverter = class extends HtmlConverter {
175
+ async convert(source, options) {
176
+ if (options.file_extension?.toLowerCase() !== ".docx") return null;
177
+ const html = await mammoth.default.convertToHtml({ arrayBuffer: toArrayBuffer(source) }, {
178
+ styleMap: options.styleMap,
179
+ includeEmbeddedStyleMap: options.includeEmbeddedStyleMap,
180
+ includeDefaultStyleMap: options.includeDefaultStyleMap,
181
+ ignoreEmptyParagraphs: options.ignoreEmptyParagraphs,
182
+ idPrefix: options.idPrefix,
183
+ transformDocument: options.transformDocument
184
+ });
185
+ return this.convertHtml(html.value);
186
+ }
187
+ };
188
+ //#endregion
189
+ //#region src/converters/ipynb.ts
190
+ var IpynbConverter = class {
191
+ async convert(source, options = {}) {
192
+ if (options.file_extension?.toLowerCase() !== ".ipynb") return null;
193
+ const notebook = JSON.parse(decodeText(source));
194
+ const output = [];
195
+ let title = notebook.metadata?.title ?? null;
196
+ for (const cell of notebook.cells ?? []) {
197
+ const lines = cell.source ?? [];
198
+ if (cell.cell_type === "markdown") {
199
+ output.push(lines.join(""));
200
+ title ?? (title = lines.find((line) => line.startsWith("# "))?.slice(2).trim() ?? null);
201
+ } else if (cell.cell_type === "code") output.push(`\`\`\`python\n${lines.join("")}\n\`\`\``);
202
+ else if (cell.cell_type === "raw") output.push(`\`\`\`\n${lines.join("")}\n\`\`\``);
203
+ }
204
+ const markdown = output.join("\n\n");
205
+ return {
206
+ title,
207
+ markdown,
208
+ text_content: markdown
209
+ };
210
+ }
211
+ };
212
+ //#endregion
213
+ //#region src/mime.ts
214
+ const MIME_EXTENSIONS = {
215
+ "application/atom+xml": ".atom",
216
+ "application/json": ".json",
217
+ "application/rss+xml": ".rss",
218
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
219
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
220
+ "application/xml": ".xml",
221
+ "text/csv": ".csv",
222
+ "text/html": ".html",
223
+ "text/markdown": ".md",
224
+ "text/plain": ".txt",
225
+ "text/xml": ".xml"
226
+ };
227
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
228
+ ".csv",
229
+ ".json",
230
+ ".log",
231
+ ".markdown",
232
+ ".md",
233
+ ".text",
234
+ ".txt"
235
+ ]);
236
+ function extensionFromMime(contentType) {
237
+ if (!contentType) return null;
238
+ const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
239
+ return MIME_EXTENSIONS[mime] ?? (mime.startsWith("text/") ? ".txt" : null);
240
+ }
241
+ function isTextExtension(extension) {
242
+ return TEXT_EXTENSIONS.has(extension.toLowerCase());
243
+ }
244
+ function extensionFromPathname(pathname) {
245
+ const filename = pathname.split("/").pop() ?? "";
246
+ const dot = filename.lastIndexOf(".");
247
+ return dot > 0 ? filename.slice(dot).toLowerCase() : null;
248
+ }
249
+ //#endregion
250
+ //#region src/converters/plain-text.ts
251
+ var PlainTextConverter = class {
252
+ async convert(source, options = {}) {
253
+ if (!isTextExtension(options.file_extension ?? "")) return null;
254
+ const content = decodeText(source);
255
+ return {
256
+ title: null,
257
+ markdown: content,
258
+ text_content: content
259
+ };
260
+ }
261
+ };
262
+ //#endregion
263
+ //#region src/converters/wikipedia.ts
264
+ const WIKIPEDIA_URL = /^https?:\/\/[a-z]{2,3}\.wikipedia\.org\//i;
265
+ var WikipediaConverter = class {
266
+ async convert(source, options = {}) {
267
+ const extension = options.file_extension?.toLowerCase() ?? "";
268
+ if (extension !== ".html" && extension !== ".htm" || !WIKIPEDIA_URL.test(options.url ?? "")) return null;
269
+ const document = parseHtmlDocument(decodeText(source));
270
+ document.querySelectorAll("script, style").forEach((element) => {
271
+ element.remove();
272
+ });
273
+ const body = document.querySelector("div#mw-content-text");
274
+ const title = document.querySelector("span.mw-page-title-main")?.textContent?.trim() || document.title || null;
275
+ const markdown = body ? `# ${title ?? "Wikipedia"}\n\n${new CustomTurnDown().convertSoup(body)}` : new CustomTurnDown().convertSoup(document);
276
+ return {
277
+ title,
278
+ markdown,
279
+ text_content: markdown
280
+ };
281
+ }
282
+ };
283
+ //#endregion
284
+ //#region src/converters/xlsx.ts
285
+ var XlsxConverter = class extends HtmlConverter {
286
+ async convert(source, options) {
287
+ if (options.file_extension?.toLowerCase() !== ".xlsx") return null;
288
+ const workbook = xlsx.read(source, { type: "array" });
289
+ const sections = [];
290
+ for (const sheetName of workbook.SheetNames) {
291
+ const sheet = workbook.Sheets[sheetName];
292
+ if (sheet?.["!ref"]) {
293
+ const result = await this.convertHtml(xlsx.utils.sheet_to_html(sheet));
294
+ sections.push(`## ${sheetName}\n${result?.markdown.trim() ?? ""}`);
295
+ }
296
+ }
297
+ const markdown = sections.join("\n\n");
298
+ return {
299
+ title: workbook.Props?.Title || "Untitled",
300
+ markdown,
301
+ text_content: markdown
302
+ };
303
+ }
304
+ };
305
+ //#endregion
306
+ //#region src/converters/xml-rss-atom.ts
307
+ var RSSConverter = class {
308
+ async convert(source, options = {}) {
309
+ const extension = options.file_extension?.toLowerCase() ?? "";
310
+ if (![
311
+ ".xml",
312
+ ".rss",
313
+ ".atom"
314
+ ].includes(extension)) return null;
315
+ const document = new _xmldom_xmldom.DOMParser().parseFromString(decodeText(source), "text/xml");
316
+ if (document.getElementsByTagName("rss").length > 0) return this.parseRss(document);
317
+ if (document.getElementsByTagName("feed")[0]?.getElementsByTagName("entry").length) return this.parseAtom(document);
318
+ return null;
319
+ }
320
+ parseAtom(document) {
321
+ const root = document.getElementsByTagName("feed")[0];
322
+ if (!root) return null;
323
+ const title = getText(root, "title");
324
+ const subtitle = getText(root, "subtitle");
325
+ const sections = [`# ${title ?? "Feed"}`];
326
+ if (subtitle) sections.push(subtitle);
327
+ for (const entry of [...root.getElementsByTagName("entry")]) {
328
+ const entryTitle = getText(entry, "title");
329
+ const updated = getText(entry, "updated");
330
+ const summary = getText(entry, "summary");
331
+ const content = getText(entry, "content");
332
+ const lines = [];
333
+ if (entryTitle) lines.push(`## ${entryTitle}`);
334
+ if (updated) lines.push(`Updated on: ${updated}`);
335
+ if (summary) lines.push(parseContent(summary));
336
+ if (content) lines.push(parseContent(content));
337
+ sections.push(lines.join("\n"));
338
+ }
339
+ const markdown = sections.join("\n\n");
340
+ return {
341
+ title,
342
+ markdown,
343
+ text_content: markdown
344
+ };
345
+ }
346
+ parseRss(document) {
347
+ const channel = document.getElementsByTagName("rss")[0]?.getElementsByTagName("channel")[0];
348
+ if (!channel) return null;
349
+ const title = getText(channel, "title");
350
+ const description = getText(channel, "description");
351
+ const sections = title ? [`# ${title}`] : [];
352
+ if (description) sections.push(description);
353
+ for (const item of [...channel.getElementsByTagName("item")]) {
354
+ const itemTitle = getText(item, "title");
355
+ const published = getText(item, "pubDate");
356
+ const itemDescription = getText(item, "description");
357
+ const content = getText(item, "content:encoded");
358
+ const lines = [];
359
+ if (itemTitle) lines.push(`## ${itemTitle}`);
360
+ if (published) lines.push(`Published on: ${published}`);
361
+ if (itemDescription) lines.push(parseContent(itemDescription));
362
+ if (content) lines.push(parseContent(content));
363
+ sections.push(lines.join("\n"));
364
+ }
365
+ const markdown = sections.join("\n\n");
366
+ return {
367
+ title,
368
+ markdown,
369
+ text_content: markdown
370
+ };
371
+ }
372
+ };
373
+ function parseContent(content) {
374
+ return new CustomTurnDown().convertSoup(parseHtmlDocument(content).documentElement);
375
+ }
376
+ function getText(element, tagName) {
377
+ return element.getElementsByTagName(tagName)[0]?.textContent?.trim() || null;
378
+ }
379
+ //#endregion
380
+ //#region src/converters/youtube.ts
381
+ var YouTubeConverter = class {
382
+ async convert(source, options = {}) {
383
+ const extension = options.file_extension?.toLowerCase() ?? "";
384
+ if (extension !== ".html" && extension !== ".htm" || !options.url?.startsWith("https://www.youtube.com/watch?")) return null;
385
+ const document = parseHtmlDocument(decodeText(source));
386
+ const metadata = { title: document.title };
387
+ document.querySelectorAll("meta").forEach((meta) => {
388
+ const content = meta.getAttribute("content");
389
+ const key = meta.getAttribute("itemprop") ?? meta.getAttribute("property") ?? meta.getAttribute("name");
390
+ if (key && content) metadata[key] = content;
391
+ });
392
+ try {
393
+ for (const script of document.querySelectorAll("script")) {
394
+ const content = script.textContent ?? "";
395
+ if (!content.includes("ytInitialData")) continue;
396
+ const start = content.indexOf("{");
397
+ const end = content.lastIndexOf("}");
398
+ if (start >= 0 && end > start) {
399
+ const description = findKey(JSON.parse(content.slice(start, end + 1)), "attributedDescriptionBodyText");
400
+ if (isRecord(description) && typeof description.content === "string") metadata.description = description.content;
401
+ }
402
+ break;
403
+ }
404
+ } catch {}
405
+ const title = first(metadata, [
406
+ "title",
407
+ "og:title",
408
+ "name"
409
+ ]) || document.title || null;
410
+ const lines = ["# YouTube"];
411
+ if (title) lines.push(`## ${title}`);
412
+ const stats = [];
413
+ const views = first(metadata, ["interactionCount"]);
414
+ const keywords = first(metadata, ["keywords"]);
415
+ const runtime = first(metadata, ["duration"]);
416
+ if (views) stats.push(`- **Views:** ${views}`);
417
+ if (keywords) stats.push(`- **Keywords:** ${keywords}`);
418
+ if (runtime) stats.push(`- **Runtime:** ${runtime}`);
419
+ if (stats.length > 0) lines.push(`### Video Metadata\n${stats.join("\n")}`);
420
+ const description = first(metadata, ["description", "og:description"]);
421
+ if (description) lines.push(`### Description\n${description}`);
422
+ const markdown = lines.join("\n\n");
423
+ return {
424
+ title,
425
+ markdown,
426
+ text_content: markdown
427
+ };
428
+ }
429
+ };
430
+ function first(metadata, keys) {
431
+ for (const key of keys) if (metadata[key]) return metadata[key];
432
+ return null;
433
+ }
434
+ function findKey(value, key) {
435
+ if (Array.isArray(value)) for (const item of value) {
436
+ const match = findKey(item, key);
437
+ if (match !== void 0) return match;
438
+ }
439
+ else if (isRecord(value)) {
440
+ if (key in value) return value[key];
441
+ for (const item of Object.values(value)) {
442
+ const match = findKey(item, key);
443
+ if (match !== void 0) return match;
444
+ }
445
+ }
446
+ }
447
+ function isRecord(value) {
448
+ return typeof value === "object" && value !== null;
449
+ }
450
+ //#endregion
451
+ //#region src/markitdown.ts
452
+ var MarkItDown = class {
453
+ constructor() {
454
+ this.converters = [];
455
+ this.registerConverter(new PlainTextConverter());
456
+ this.registerConverter(new HtmlConverter());
457
+ this.registerConverter(new RSSConverter());
458
+ this.registerConverter(new WikipediaConverter());
459
+ this.registerConverter(new YouTubeConverter());
460
+ this.registerConverter(new BingSerpConverter());
461
+ this.registerConverter(new DocxConverter());
462
+ this.registerConverter(new XlsxConverter());
463
+ this.registerConverter(new IpynbConverter());
464
+ }
465
+ /** Convert a URL, response, blob, array buffer, data view, or typed array. */
466
+ async convert(source, options = {}) {
467
+ if (typeof source === "string" || source instanceof URL) return this.convertUrl(source.toString(), options);
468
+ if (isResponse(source)) return this.convertResponse(source, options);
469
+ const extensions = /* @__PURE__ */ new Set();
470
+ this.addExtension(extensions, options.file_extension);
471
+ if (isBlob(source)) {
472
+ this.addExtension(extensions, extensionFromMime(source.type));
473
+ if ("name" in source && typeof source.name === "string") this.addExtension(extensions, extensionFromPathname(source.name));
474
+ }
475
+ if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` when converting bytes.");
476
+ return this.convertBytes(await toUint8Array(source), extensions, options);
477
+ }
478
+ /**
479
+ * @deprecated Pass the byte source directly to `convert`.
480
+ */
481
+ async convertBuffer(source, options) {
482
+ return this.convert(source, options);
483
+ }
484
+ async convertUrl(source, { fetch: fetchImplementation = globalThis.fetch, ...options }) {
485
+ const url = new URL(source);
486
+ if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`Unsupported URL protocol: ${url.protocol}`);
487
+ if (!fetchImplementation) throw new Error("No fetch implementation is available.");
488
+ const response = await fetchImplementation(url);
489
+ if (!response.ok) throw new Error(`Failed to fetch URL: ${url}, status: ${response.status}`);
490
+ return this.convertResponse(response, {
491
+ ...options,
492
+ url: url.toString()
493
+ });
494
+ }
495
+ async convertResponse(response, options) {
496
+ const extensions = /* @__PURE__ */ new Set();
497
+ this.addExtension(extensions, options.file_extension);
498
+ this.addExtension(extensions, extensionFromMime(response.headers.get("content-type")));
499
+ const filename = (response.headers.get("content-disposition") ?? "").match(/filename\*?=(?:UTF-8''|["']?)([^;"']+)/i)?.[1];
500
+ if (filename) this.addExtension(extensions, extensionFromPathname(decodeURIComponent(filename.trim())));
501
+ const responseUrl = options.url || response.url;
502
+ if (responseUrl) this.addExtension(extensions, extensionFromPathname(new URL(responseUrl).pathname));
503
+ if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` or a Content-Type header.");
504
+ return this.convertBytes(new Uint8Array(await response.arrayBuffer()), extensions, {
505
+ ...options,
506
+ url: responseUrl
507
+ });
508
+ }
509
+ async convertBytes(source, extensions, options) {
510
+ let lastError;
511
+ for (const extension of extensions) for (const converter of this.converters) try {
512
+ const result = await converter.convert(source, {
513
+ ...options,
514
+ file_extension: extension,
515
+ _parent_converters: this.converters
516
+ });
517
+ if (result) {
518
+ result.markdown = result.markdown.replace(/\r\n|\r|\n/g, "\n").trim().replace(/\n{3,}/g, "\n\n");
519
+ result.text_content = result.markdown;
520
+ return result;
521
+ }
522
+ } catch (error) {
523
+ lastError = error;
524
+ }
525
+ const formats = [...extensions].join(", ");
526
+ if (lastError) throw new Error(`Could not convert ${formats} to Markdown: ${String(lastError)}`);
527
+ throw new Error(`Unsupported file type: ${formats}.`);
528
+ }
529
+ addExtension(extensions, extension) {
530
+ if (!extension) return;
531
+ extensions.add(extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`);
532
+ }
533
+ registerConverter(converter) {
534
+ this.converters.unshift(converter);
535
+ }
536
+ };
537
+ function isResponse(value) {
538
+ return typeof Response !== "undefined" && value instanceof Response;
539
+ }
540
+ //#endregion
541
+ exports.MarkItDown = MarkItDown;
542
+
543
+ //# sourceMappingURL=index.cjs.map