@ryangarber/markitdown-ts 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,17 @@
1
- import turndownPluginGfm from "@joplin/turndown-plugin-gfm";
2
- import TurndownService from "turndown";
3
1
  import { parseHTML } from "linkedom";
2
+ import TurndownPluginGfm from "@truto/turndown-plugin-gfm";
3
+ import TurndownService from "turndown";
4
+ import { fileTypeFromBuffer } from "file-type";
5
+ import { Mime } from "mime";
6
+ import otherTypes from "mime/types/other.js";
7
+ import standardTypes from "mime/types/standard.js";
4
8
  import mammoth from "mammoth";
5
9
  import { extractText, getDocumentProxy, getMeta } from "unpdf";
6
10
  import { pptxToHtml } from "@jvmr/pptx-to-html";
7
11
  import { DOMParser, Document, Element, Node } from "@xmldom/xmldom";
8
12
  import readExcelFile from "read-excel-file/universal";
9
13
  import { unzipSync } from "fflate";
10
- //#region src/bytes.ts
14
+ // src/utils/bytes.ts
11
15
  async function toUint8Array(source) {
12
16
  if (source instanceof Uint8Array) return source;
13
17
  if (isBlob(source)) return new Uint8Array(await source.arrayBuffer());
@@ -23,12 +27,11 @@ function decodeText(source) {
23
27
  function isBlob(value) {
24
28
  return typeof Blob !== "undefined" && value instanceof Blob;
25
29
  }
26
- //#endregion
27
- //#region src/custom-turndown.ts
28
- var CustomTurnDown = class {
30
+ // src/utils/turndown.ts
31
+ var Turndown = class {
29
32
  convertSoup(document) {
30
33
  const service = new TurndownService({ headingStyle: "atx" });
31
- service.use(turndownPluginGfm.gfm);
34
+ service.use(TurndownPluginGfm);
32
35
  service.addRule("anchor tags", {
33
36
  filter: ["a"],
34
37
  replacement(content, node) {
@@ -63,48 +66,166 @@ var CustomTurnDown = class {
63
66
  return `![${alt}](${source}${title ? ` "${title}"` : ""})`;
64
67
  }
65
68
  });
66
- return service.turndown(document);
69
+ return service.turndown(serializeNode(document));
67
70
  }
68
71
  };
69
- //#endregion
70
- //#region src/html-document.ts
71
- function parseHtmlDocument(html) {
72
- return parseHTML(html).document;
72
+ /**
73
+ * Turndown and its GFM plugin rely on DOM APIs (e.g. `HTMLTableElement.rows`, `Element.closest`)
74
+ * that linkedom does not implement, which silently drops content such as tables. Serializing
75
+ * lets Turndown re-parse the markup with its own spec-compliant DOM.
76
+ */
77
+ function serializeNode(node) {
78
+ if (typeof node === "string") return node;
79
+ if (node.nodeType === node.DOCUMENT_NODE) return node.documentElement?.outerHTML ?? "";
80
+ if (node.nodeType === node.ELEMENT_NODE) return node.outerHTML;
81
+ if (node.nodeType === node.TEXT_NODE) return escapeHtml$1(node.textContent ?? "");
82
+ return [...node.childNodes].map(serializeNode).join("");
83
+ }
84
+ function escapeHtml$1(value) {
85
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
86
+ }
87
+ // src/utils/html.ts
88
+ function parseHtmlDocument(html, transform) {
89
+ const document = parseHTML(wrapFragment(html)).document;
90
+ return Object.fromEntries(Object.entries(transform(document)).map(([key, value]) => {
91
+ if (value.type === "node") return [key, (Array.isArray(value.value) ? value.value : [value.value]).map((node) => new Turndown().convertSoup(node).split(/\n+/).map((line) => line.trim()).filter(Boolean).join("\n\n")).join("\n\n")];
92
+ return [key, value.value];
93
+ }));
94
+ }
95
+ /**
96
+ * linkedom only parses complete documents: given a fragment (e.g. mammoth output or an RSS
97
+ * description), it promotes the first element to `documentElement` and drops its siblings
98
+ * from the tree, so wrap fragments in a full document first.
99
+ */
100
+ function wrapFragment(html) {
101
+ if (/<html[\s>]/i.test(html)) return html;
102
+ if (/<(head|body)[\s>]/i.test(html)) return `<html>${html}</html>`;
103
+ return `<html><head></head><body>${html}</body></html>`;
104
+ }
105
+ // src/utils/mime.ts
106
+ const mime = new Mime(standardTypes, otherTypes);
107
+ function extensionFromMime(contentType) {
108
+ if (!contentType) return null;
109
+ return extensionNormalized(mime.getExtension(contentType));
110
+ }
111
+ async function extensionFromBuffer(buffer) {
112
+ const type = await fileTypeFromBuffer(buffer);
113
+ if (!type) return null;
114
+ return extensionNormalized(type.ext);
115
+ }
116
+ /**
117
+ * Sniffs text formats that have no magic number. Returns null for binary content, and `txt`
118
+ * for text that is not recognized as anything more specific.
119
+ */
120
+ function extensionFromText(buffer) {
121
+ let text;
122
+ try {
123
+ text = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
124
+ } catch {
125
+ return null;
126
+ }
127
+ if (hasBinaryControlCharacters(text)) return null;
128
+ const start = text.trimStart().slice(0, 1024).toLowerCase();
129
+ if (start.startsWith("<")) {
130
+ if (/<rss[\s>]/.test(start)) return "rss";
131
+ if (/<feed[\s>]/.test(start)) return "atom";
132
+ if (/^<!doctype html|<(html|head|body)[\s>]/.test(start)) return "html";
133
+ if (start.startsWith("<?xml")) return "xml";
134
+ }
135
+ if (start.startsWith("{") && /"nbformat"\s*:/.test(text) && /"cells"\s*:/.test(text)) return "ipynb";
136
+ return "txt";
137
+ }
138
+ function hasBinaryControlCharacters(text) {
139
+ for (let index = 0; index < text.length; index++) {
140
+ const code = text.charCodeAt(index);
141
+ if (code < 32 && (code < 9 || code > 13) && code !== 27) return true;
142
+ }
143
+ return false;
144
+ }
145
+ function extensionFromPathname(pathname) {
146
+ const filename = pathname.split("/").pop() ?? "";
147
+ const dot = filename.lastIndexOf(".");
148
+ return dot > 0 ? extensionNormalized(filename.slice(dot)) : null;
149
+ }
150
+ /** Reads the filename from a Content-Disposition header, preferring the RFC 5987 form. */
151
+ function filenameFromDisposition(disposition) {
152
+ if (!disposition) return null;
153
+ const extended = disposition.match(/filename\*\s*=\s*[\w-]*'[^']*'([^;]+)/i)?.[1]?.trim();
154
+ if (extended) try {
155
+ return decodeURIComponent(extended);
156
+ } catch {
157
+ return extended;
158
+ }
159
+ const plain = disposition.match(/filename\s*=\s*(?:"([^"]*)"|([^;]+))/i);
160
+ return (plain?.[1] ?? plain?.[2])?.trim() || null;
161
+ }
162
+ /**
163
+ * Lists candidate extensions for the content, most reliable first: explicit extensions, then
164
+ * names, then declared MIME types, then the content itself.
165
+ */
166
+ async function detectExtensions(buffer, { extension, filename, urls = [], contentType } = {}) {
167
+ const candidates = [
168
+ extensionNormalized(extension),
169
+ filename ? extensionFromPathname(filename) : null,
170
+ ...urls.map((url) => url ? extensionFromUrl(url) : null),
171
+ extensionFromMime(contentType),
172
+ await extensionFromBuffer(buffer) ?? extensionFromText(buffer)
173
+ ];
174
+ return [...new Set(candidates.filter((candidate) => !!candidate))];
175
+ }
176
+ function extensionFromUrl(url) {
177
+ try {
178
+ return extensionFromPathname(decodeURIComponent(new URL(url).pathname));
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+ function extensionNormalized(extension, withDot = false) {
184
+ if (!extension) return "";
185
+ const normalized = extension.trim().toLowerCase().replace(/^[.-]+/, "");
186
+ return withDot ? `.${normalized}` : normalized;
187
+ }
188
+ function isTextExtension(extension) {
189
+ const type = mime.getType(extension);
190
+ if (!type) return false;
191
+ return type.startsWith("text/");
73
192
  }
74
- //#endregion
75
- //#region src/converters/bingserp.ts
193
+ // src/converters/bingserp.ts
76
194
  const BING_SEARCH_URL = /^https:\/\/www\.bing\.com\/search\?q=/i;
77
195
  const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
78
196
  var BingSerpConverter = class {
79
197
  async convert(source, options = {}) {
80
- const extension = options.file_extension?.toLowerCase() ?? "";
81
- const url = options.url ?? "";
82
- if (extension !== ".html" && extension !== ".htm" || !BING_SEARCH_URL.test(url)) return null;
83
- const document = parseHtmlDocument(decodeText(source));
84
- const query = new URL(url).searchParams.get("q") ?? "";
85
- document.querySelectorAll(".tptt").forEach((element) => {
86
- if (element.textContent) element.textContent += " ";
87
- });
88
- document.querySelectorAll(".algoSlug_icon").forEach((element) => {
89
- element.remove();
90
- });
91
- const turndown = new CustomTurnDown();
92
- const markdown = `## A Bing search for '${query}' found the following results:\n\n${[...document.querySelectorAll(".b_algo")].map((result) => {
93
- result.querySelectorAll("a[href]").forEach((anchor) => {
94
- const href = anchor.getAttribute("href");
95
- if (!href) return;
96
- try {
97
- const encoded = new URL(href, url).searchParams.get("u");
98
- if (encoded) anchor.setAttribute("href", decodeBingUrl(encoded));
99
- } catch {}
198
+ if (options.extension !== "html" && options.extension !== "htm" || !BING_SEARCH_URL.test(options.url ?? "")) return null;
199
+ let query = new URL(options.url ?? "").searchParams.get("q") || null;
200
+ if (query) query = `Search results for "${query}"`;
201
+ return parseHtmlDocument(decodeText(source), (document) => {
202
+ document.querySelectorAll(".tptt").forEach((element) => {
203
+ if (element.textContent) element.textContent += " ";
100
204
  });
101
- return turndown.convertSoup(result).split(/\n+/).map((line) => line.trim()).filter(Boolean).join("\n");
102
- }).join("\n\n")}`;
103
- return {
104
- title: document.title || null,
105
- markdown,
106
- text_content: markdown
107
- };
205
+ document.querySelectorAll(".algoSlug_icon").forEach((element) => {
206
+ element.remove();
207
+ });
208
+ return {
209
+ title: {
210
+ type: "metadata",
211
+ value: query || document.title || null
212
+ },
213
+ markdown: {
214
+ type: "node",
215
+ value: [...document.querySelectorAll(".b_algo")].map((result) => {
216
+ result.querySelectorAll("a[href]").forEach((anchor) => {
217
+ const href = anchor.getAttribute("href");
218
+ if (!href) return;
219
+ try {
220
+ const encoded = new URL(href, options.url ?? "").searchParams.get("u");
221
+ if (encoded) anchor.setAttribute("href", decodeBingUrl(encoded));
222
+ } catch {}
223
+ });
224
+ return result;
225
+ })
226
+ }
227
+ };
228
+ });
108
229
  }
109
230
  };
110
231
  function decodeBingUrl(encoded) {
@@ -124,52 +245,46 @@ function decodeBingUrl(encoded) {
124
245
  }
125
246
  return new TextDecoder().decode(new Uint8Array(bytes));
126
247
  }
127
- //#endregion
128
- //#region src/converters/html.ts
248
+ // src/converters/html.ts
129
249
  var HtmlConverter = class {
130
250
  async convert(source, options) {
131
- const extension = options.file_extension?.toLowerCase() ?? "";
132
- if (extension !== ".html" && extension !== ".htm") return null;
251
+ if (options.extension !== "html" && options.extension !== "htm") return null;
133
252
  return this.convertHtml(decodeText(source));
134
253
  }
135
254
  async convertHtml(html) {
136
- const document = parseHtmlDocument(html);
137
- const title = document.title;
138
- document.querySelectorAll("script, style, title").forEach((element) => {
139
- element.remove();
255
+ return parseHtmlDocument(html, (document) => {
256
+ const title = document.title || null;
257
+ document.querySelectorAll("script, style, title").forEach((element) => {
258
+ element.remove();
259
+ });
260
+ return {
261
+ title: {
262
+ type: "metadata",
263
+ value: title
264
+ },
265
+ markdown: {
266
+ type: "node",
267
+ value: document
268
+ }
269
+ };
140
270
  });
141
- const markdown = new CustomTurnDown().convertSoup(document);
142
- return {
143
- title: title || null,
144
- markdown,
145
- text_content: markdown
146
- };
147
271
  }
148
272
  };
149
- //#endregion
150
- //#region src/converters/docx.ts
273
+ // src/converters/docx.ts
151
274
  var DocxConverter = class extends HtmlConverter {
152
275
  async convert(source, options) {
153
- if (options.file_extension?.toLowerCase() !== ".docx") return null;
276
+ if (options.extension !== "docx") return null;
154
277
  let mammothInput = { arrayBuffer: toArrayBuffer(source) };
155
278
  if (typeof process !== "undefined" && process.versions !== null && process.versions.node !== null) mammothInput = { buffer: Buffer.from(source) };
156
- const mammothOptions = {
157
- styleMap: options.styleMap,
158
- includeEmbeddedStyleMap: options.includeEmbeddedStyleMap,
159
- includeDefaultStyleMap: options.includeDefaultStyleMap,
160
- ignoreEmptyParagraphs: options.ignoreEmptyParagraphs,
161
- idPrefix: options.idPrefix
162
- };
163
- if (options.transformDocument) mammothOptions.transformDocument = options.transformDocument;
279
+ const mammothOptions = Object.fromEntries(Object.entries(options.mammoth ?? {}).flatMap(([key, value]) => value !== void 0 ? [[key, value]] : []));
164
280
  const html = await mammoth.convertToHtml(mammothInput, mammothOptions);
165
281
  return this.convertHtml(html.value);
166
282
  }
167
283
  };
168
- //#endregion
169
- //#region src/converters/ipynb.ts
284
+ // src/converters/ipynb.ts
170
285
  var IpynbConverter = class {
171
286
  async convert(source, options = {}) {
172
- if (options.file_extension?.toLowerCase() !== ".ipynb") return null;
287
+ if (options.extension !== "ipynb") return null;
173
288
  const notebook = JSON.parse(decodeText(source));
174
289
  const output = [];
175
290
  let title = notebook.metadata?.title ?? null;
@@ -184,99 +299,49 @@ var IpynbConverter = class {
184
299
  const markdown = output.join("\n\n");
185
300
  return {
186
301
  title,
187
- markdown,
188
- text_content: markdown
302
+ markdown
189
303
  };
190
304
  }
191
305
  };
192
- //#endregion
193
- //#region src/converters/pdf.ts
306
+ // src/converters/pdf.ts
194
307
  var PdfConverter = class {
195
308
  async convert(source, options) {
196
- if (options.file_extension?.toLowerCase() !== ".pdf") return null;
309
+ if (options.extension !== "pdf") return null;
197
310
  const pdf = await getDocumentProxy(source.slice());
198
311
  try {
199
312
  const [{ text }, { info }] = await Promise.all([extractText(pdf, { mergePages: true }), getMeta(pdf)]);
200
313
  return {
201
314
  title: typeof info.Title === "string" && info.Title.trim() ? info.Title : null,
202
- markdown: text,
203
- text_content: text
315
+ markdown: text
204
316
  };
205
317
  } finally {
206
318
  await pdf.loadingTask.destroy();
207
319
  }
208
320
  }
209
321
  };
210
- //#endregion
211
- //#region src/mime.ts
212
- const MIME_EXTENSIONS = {
213
- "application/atom+xml": ".atom",
214
- "application/json": ".json",
215
- "application/pdf": ".pdf",
216
- "application/zip": ".zip",
217
- "application/rss+xml": ".rss",
218
- "application/x-pdf": ".pdf",
219
- "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
220
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
221
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
222
- "application/xml": ".xml",
223
- "text/csv": ".csv",
224
- "text/html": ".html",
225
- "text/markdown": ".md",
226
- "text/plain": ".txt",
227
- "text/xml": ".xml"
228
- };
229
- const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
230
- ".csv",
231
- ".json",
232
- ".log",
233
- ".markdown",
234
- ".md",
235
- ".text",
236
- ".txt"
237
- ]);
238
- function extensionFromMime(contentType) {
239
- if (!contentType) return null;
240
- const mime = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
241
- return MIME_EXTENSIONS[mime] ?? (mime.startsWith("text/") ? ".txt" : null);
242
- }
243
- function isTextExtension(extension) {
244
- return TEXT_EXTENSIONS.has(extension.toLowerCase());
245
- }
246
- function extensionFromPathname(pathname) {
247
- const filename = pathname.split("/").pop() ?? "";
248
- const dot = filename.lastIndexOf(".");
249
- return dot > 0 ? filename.slice(dot).toLowerCase() : null;
250
- }
251
- //#endregion
252
- //#region src/converters/plain-text.ts
322
+ // src/converters/plain-text.ts
253
323
  var PlainTextConverter = class {
254
324
  async convert(source, options = {}) {
255
- if (!isTextExtension(options.file_extension ?? "")) return null;
256
- const content = decodeText(source);
325
+ if (!isTextExtension(options.extension ?? "")) return null;
257
326
  return {
258
327
  title: null,
259
- markdown: content,
260
- text_content: content
328
+ markdown: decodeText(source)
261
329
  };
262
330
  }
263
331
  };
264
- //#endregion
265
- //#region src/converters/pptx.ts
332
+ // src/converters/pptx.ts
266
333
  var PptxConverter = class extends HtmlConverter {
267
334
  async convert(source, options) {
268
- if (options.file_extension?.toLowerCase() !== ".pptx") return null;
335
+ if (options.extension !== "pptx") return null;
269
336
  const slides = await pptxToHtml(toArrayBuffer(source), { domParserFactory: createPptxDomParser });
270
337
  const sections = [];
271
338
  for (const [index, html] of slides.entries()) {
272
339
  const result = await this.convertHtml(`<html><body>${html}</body></html>`);
273
340
  sections.push(`<!-- Slide number: ${index + 1} -->\n${result?.markdown.trim() ?? ""}`);
274
341
  }
275
- const markdown = sections.join("\n\n");
276
342
  return {
277
343
  title: null,
278
- markdown,
279
- text_content: markdown
344
+ markdown: sections.join("\n\n")
280
345
  };
281
346
  }
282
347
  };
@@ -340,32 +405,33 @@ function defineGetter(target, name, get) {
340
405
  get
341
406
  });
342
407
  }
343
- //#endregion
344
- //#region src/converters/wikipedia.ts
408
+ // src/converters/wikipedia.ts
345
409
  const WIKIPEDIA_URL = /^https?:\/\/[a-z]{2,3}\.wikipedia\.org\//i;
346
410
  var WikipediaConverter = class {
347
411
  async convert(source, options = {}) {
348
- const extension = options.file_extension?.toLowerCase() ?? "";
349
- if (extension !== ".html" && extension !== ".htm" || !WIKIPEDIA_URL.test(options.url ?? "")) return null;
350
- const document = parseHtmlDocument(decodeText(source));
351
- document.querySelectorAll("script, style").forEach((element) => {
352
- element.remove();
412
+ if (options.extension !== "html" && options.extension !== "htm" || !WIKIPEDIA_URL.test(options.url ?? "")) return null;
413
+ return parseHtmlDocument(decodeText(source), (document) => {
414
+ document.querySelectorAll("script, style").forEach((element) => {
415
+ element.remove();
416
+ });
417
+ const body = document.querySelector("div#mw-content-text");
418
+ return {
419
+ title: {
420
+ type: "metadata",
421
+ value: document.querySelector("span.mw-page-title-main")?.textContent?.trim() || document.title || null
422
+ },
423
+ markdown: {
424
+ type: "node",
425
+ value: body || document
426
+ }
427
+ };
353
428
  });
354
- const body = document.querySelector("div#mw-content-text");
355
- const title = document.querySelector("span.mw-page-title-main")?.textContent?.trim() || document.title || null;
356
- const markdown = body ? `# ${title ?? "Wikipedia"}\n\n${new CustomTurnDown().convertSoup(body)}` : new CustomTurnDown().convertSoup(document);
357
- return {
358
- title,
359
- markdown,
360
- text_content: markdown
361
- };
362
429
  }
363
430
  };
364
- //#endregion
365
- //#region src/converters/xlsx.ts
431
+ // src/converters/xlsx.ts
366
432
  var XlsxConverter = class extends HtmlConverter {
367
433
  async convert(source, options) {
368
- if (options.file_extension?.toLowerCase() !== ".xlsx") return null;
434
+ if (options.extension !== "xlsx") return null;
369
435
  const sheets = await readExcelFile(toArrayBuffer(source));
370
436
  const sections = [];
371
437
  for (const { sheet, data } of sheets) {
@@ -374,11 +440,9 @@ var XlsxConverter = class extends HtmlConverter {
374
440
  const result = await this.convertHtml(html);
375
441
  sections.push(`## ${sheet}\n${result?.markdown.trim() ?? ""}`);
376
442
  }
377
- const markdown = sections.join("\n\n");
378
443
  return {
379
444
  title: null,
380
- markdown,
381
- text_content: markdown
445
+ markdown: sections.join("\n\n")
382
446
  };
383
447
  }
384
448
  };
@@ -399,129 +463,126 @@ function cellText(value) {
399
463
  function escapeHtml(value) {
400
464
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
401
465
  }
402
- //#endregion
403
- //#region src/converters/xml-rss-atom.ts
466
+ // src/converters/xml-rss-atom.ts
404
467
  var RSSConverter = class {
405
468
  async convert(source, options = {}) {
406
- const extension = options.file_extension?.toLowerCase() ?? "";
407
- if (![
408
- ".xml",
409
- ".rss",
410
- ".atom"
411
- ].includes(extension)) return null;
469
+ if (options.extension !== "xml" && options.extension !== "rss" && options.extension !== "atom") return null;
412
470
  const document = new DOMParser().parseFromString(decodeText(source), "text/xml");
413
- if (document.getElementsByTagName("rss").length > 0) return this.parseRss(document);
414
- if (document.getElementsByTagName("feed")[0]?.getElementsByTagName("entry").length) return this.parseAtom(document);
471
+ if (document.getElementsByTagName("rss").length > 0) return parseRss(document);
472
+ if (document.getElementsByTagName("feed")[0]?.getElementsByTagName("entry").length) return parseAtom(document);
415
473
  return null;
416
474
  }
417
- parseAtom(document) {
418
- const root = document.getElementsByTagName("feed")[0];
419
- if (!root) return null;
420
- const title = getText(root, "title");
421
- const subtitle = getText(root, "subtitle");
422
- const sections = [`# ${title ?? "Feed"}`];
423
- if (subtitle) sections.push(subtitle);
424
- for (const entry of [...root.getElementsByTagName("entry")]) {
425
- const entryTitle = getText(entry, "title");
426
- const updated = getText(entry, "updated");
427
- const summary = getText(entry, "summary");
428
- const content = getText(entry, "content");
429
- const lines = [];
430
- if (entryTitle) lines.push(`## ${entryTitle}`);
431
- if (updated) lines.push(`Updated on: ${updated}`);
432
- if (summary) lines.push(parseContent(summary));
433
- if (content) lines.push(parseContent(content));
434
- sections.push(lines.join("\n"));
435
- }
436
- const markdown = sections.join("\n\n");
437
- return {
438
- title,
439
- markdown,
440
- text_content: markdown
441
- };
475
+ };
476
+ function parseAtom(document) {
477
+ const root = document.getElementsByTagName("feed")[0];
478
+ if (!root) return null;
479
+ const title = getText(root, "title");
480
+ const subtitle = getText(root, "subtitle");
481
+ const sections = [`# ${title ?? "Feed"}`];
482
+ if (subtitle) sections.push(subtitle);
483
+ for (const entry of [...root.getElementsByTagName("entry")]) {
484
+ const entryTitle = getText(entry, "title");
485
+ const updated = getText(entry, "updated");
486
+ const summary = getText(entry, "summary");
487
+ const content = getText(entry, "content");
488
+ const lines = [];
489
+ if (entryTitle) lines.push(`## ${entryTitle}`);
490
+ if (updated) lines.push(`Updated on: ${updated}`);
491
+ const node = summary || content;
492
+ if (node) lines.push(parseHtmlDocument(node, (document) => ({ markdown: {
493
+ type: "node",
494
+ value: document.documentElement
495
+ } })).markdown);
496
+ sections.push(lines.join("\n\n"));
442
497
  }
443
- parseRss(document) {
444
- const channel = document.getElementsByTagName("rss")[0]?.getElementsByTagName("channel")[0];
445
- if (!channel) return null;
446
- const title = getText(channel, "title");
447
- const description = getText(channel, "description");
448
- const sections = title ? [`# ${title}`] : [];
449
- if (description) sections.push(description);
450
- for (const item of [...channel.getElementsByTagName("item")]) {
451
- const itemTitle = getText(item, "title");
452
- const published = getText(item, "pubDate");
453
- const itemDescription = getText(item, "description");
454
- const content = getText(item, "content:encoded");
455
- const lines = [];
456
- if (itemTitle) lines.push(`## ${itemTitle}`);
457
- if (published) lines.push(`Published on: ${published}`);
458
- if (itemDescription) lines.push(parseContent(itemDescription));
459
- if (content) lines.push(parseContent(content));
460
- sections.push(lines.join("\n"));
461
- }
462
- const markdown = sections.join("\n\n");
463
- return {
464
- title,
465
- markdown,
466
- text_content: markdown
467
- };
498
+ return {
499
+ title,
500
+ markdown: sections.join("\n\n")
501
+ };
502
+ }
503
+ function parseRss(document) {
504
+ const channel = document.getElementsByTagName("rss")[0]?.getElementsByTagName("channel")[0];
505
+ if (!channel) return null;
506
+ const title = getText(channel, "title");
507
+ const description = getText(channel, "description");
508
+ const sections = title ? [`# ${title}`] : [];
509
+ if (description) sections.push(description);
510
+ for (const item of [...channel.getElementsByTagName("item")]) {
511
+ const itemTitle = getText(item, "title");
512
+ const published = getText(item, "pubDate");
513
+ const itemDescription = getText(item, "description");
514
+ const content = getText(item, "content:encoded");
515
+ const lines = [];
516
+ if (itemTitle) lines.push(`## ${itemTitle}`);
517
+ if (published) lines.push(`Published on: ${published}`);
518
+ const node = itemDescription || content;
519
+ if (node) lines.push(parseHtmlDocument(node, (document) => ({ markdown: {
520
+ type: "node",
521
+ value: document.documentElement
522
+ } })).markdown);
523
+ sections.push(lines.join("\n\n"));
468
524
  }
469
- };
470
- function parseContent(content) {
471
- return new CustomTurnDown().convertSoup(parseHtmlDocument(content).documentElement);
525
+ return {
526
+ title,
527
+ markdown: sections.join("\n\n")
528
+ };
472
529
  }
473
530
  function getText(element, tagName) {
474
531
  return element.getElementsByTagName(tagName)[0]?.textContent?.trim() || null;
475
532
  }
476
- //#endregion
477
- //#region src/converters/youtube.ts
533
+ // src/converters/youtube.ts
534
+ const YOUTUBE_URL = /^https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|shorts\/)?([a-zA-Z0-9_-]+)/i;
478
535
  var YouTubeConverter = class {
479
536
  async convert(source, options = {}) {
480
- const extension = options.file_extension?.toLowerCase() ?? "";
481
- if (extension !== ".html" && extension !== ".htm" || !options.url?.startsWith("https://www.youtube.com/watch?")) return null;
482
- const document = parseHtmlDocument(decodeText(source));
483
- const metadata = { title: document.title };
484
- document.querySelectorAll("meta").forEach((meta) => {
485
- const content = meta.getAttribute("content");
486
- const key = meta.getAttribute("itemprop") ?? meta.getAttribute("property") ?? meta.getAttribute("name");
487
- if (key && content) metadata[key] = content;
488
- });
489
- try {
490
- for (const script of document.querySelectorAll("script")) {
491
- const content = script.textContent ?? "";
492
- if (!content.includes("ytInitialData")) continue;
493
- const start = content.indexOf("{");
494
- const end = content.lastIndexOf("}");
495
- if (start >= 0 && end > start) {
496
- const description = findKey(JSON.parse(content.slice(start, end + 1)), "attributedDescriptionBodyText");
497
- if (isRecord(description) && typeof description.content === "string") metadata.description = description.content;
537
+ if (options.extension !== "html" && options.extension !== "htm" || !YOUTUBE_URL.test(options.url ?? "")) return null;
538
+ return parseHtmlDocument(decodeText(source), (document) => {
539
+ const metadata = {};
540
+ document.querySelectorAll("meta").forEach((meta) => {
541
+ const content = meta.getAttribute("content");
542
+ const key = meta.getAttribute("itemprop") ?? meta.getAttribute("property") ?? meta.getAttribute("name");
543
+ if (key && content) metadata[key] = content;
544
+ });
545
+ try {
546
+ for (const script of document.querySelectorAll("script")) {
547
+ const content = script.textContent ?? "";
548
+ if (!content.includes("ytInitialData")) continue;
549
+ const start = content.indexOf("{");
550
+ const end = content.lastIndexOf("}");
551
+ if (start >= 0 && end > start) {
552
+ const description = findKey(JSON.parse(content.slice(start, end + 1)), "attributedDescriptionBodyText");
553
+ if (isRecord(description) && typeof description.content === "string") metadata.description = description.content;
554
+ }
555
+ break;
498
556
  }
499
- break;
500
- }
501
- } catch {}
502
- const title = first(metadata, [
503
- "title",
504
- "og:title",
505
- "name"
506
- ]) || document.title || null;
507
- const lines = ["# YouTube"];
508
- if (title) lines.push(`## ${title}`);
509
- const stats = [];
510
- const views = first(metadata, ["interactionCount"]);
511
- const keywords = first(metadata, ["keywords"]);
512
- const runtime = first(metadata, ["duration"]);
513
- if (views) stats.push(`- **Views:** ${views}`);
514
- if (keywords) stats.push(`- **Keywords:** ${keywords}`);
515
- if (runtime) stats.push(`- **Runtime:** ${runtime}`);
516
- if (stats.length > 0) lines.push(`### Video Metadata\n${stats.join("\n")}`);
517
- const description = first(metadata, ["description", "og:description"]);
518
- if (description) lines.push(`### Description\n${description}`);
519
- const markdown = lines.join("\n\n");
520
- return {
521
- title,
522
- markdown,
523
- text_content: markdown
524
- };
557
+ } catch {}
558
+ const title = first(metadata, [
559
+ "title",
560
+ "og:title",
561
+ "name"
562
+ ]) || document.title || null;
563
+ const lines = ["# YouTube"];
564
+ if (title) lines.push(`## ${title}`);
565
+ const stats = [];
566
+ const views = first(metadata, ["interactionCount"]);
567
+ const keywords = first(metadata, ["keywords"]);
568
+ const runtime = first(metadata, ["duration"]);
569
+ if (views) stats.push(`- **Views:** ${views}`);
570
+ if (keywords) stats.push(`- **Keywords:** ${keywords}`);
571
+ if (runtime) stats.push(`- **Runtime:** ${runtime}`);
572
+ if (stats.length > 0) lines.push(`### Video Metadata\n\n${stats.join("\n")}`);
573
+ const description = first(metadata, ["description", "og:description"]);
574
+ if (description) lines.push(`### Description\n\n${description}`);
575
+ return {
576
+ title: {
577
+ type: "metadata",
578
+ value: title
579
+ },
580
+ markdown: {
581
+ type: "metadata",
582
+ value: lines.join("\n\n")
583
+ }
584
+ };
585
+ });
525
586
  }
526
587
  };
527
588
  function first(metadata, keys) {
@@ -544,23 +605,20 @@ function findKey(value, key) {
544
605
  function isRecord(value) {
545
606
  return typeof value === "object" && value !== null;
546
607
  }
547
- //#endregion
548
- //#region src/converters/zip.ts
608
+ // src/converters/zip.ts
549
609
  var ZipConverter = class {
550
610
  async convert(source, options = {}) {
551
- if (options.file_extension?.toLowerCase() !== ".zip") return null;
611
+ if (options.extension !== "zip") return null;
552
612
  const files = unzipSync(source, { filter: (file) => !file.name.endsWith("/") });
553
- const converters = options._parent_converters ?? [];
613
+ const converters = options._converters ?? [];
554
614
  const sections = [`Content from the zip file \`${options.url ?? "archive.zip"}\`:`];
555
615
  for (const [name, bytes] of Object.entries(files)) {
556
616
  const result = await convertEntry(bytes, name, converters, options);
557
617
  if (result) sections.push(`## File: ${name}\n\n${result.markdown}`);
558
618
  }
559
- const markdown = sections.join("\n\n");
560
619
  return {
561
620
  title: null,
562
- markdown,
563
- text_content: markdown
621
+ markdown: sections.join("\n\n")
564
622
  };
565
623
  }
566
624
  };
@@ -569,9 +627,9 @@ async function convertEntry(source, name, converters, options) {
569
627
  if (!extension) return null;
570
628
  const entryOptions = {
571
629
  ...options,
572
- file_extension: extension,
630
+ extension,
573
631
  url: void 0,
574
- _parent_converters: converters
632
+ _converters: converters
575
633
  };
576
634
  for (const converter of converters) try {
577
635
  const result = await converter.convert(source, entryOptions);
@@ -579,42 +637,72 @@ async function convertEntry(source, name, converters, options) {
579
637
  } catch {}
580
638
  return null;
581
639
  }
582
- //#endregion
583
- //#region src/markitdown.ts
640
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/typeof.js
641
+ function _typeof(o) {
642
+ "@babel/helpers - typeof";
643
+ return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(o) {
644
+ return typeof o;
645
+ } : function(o) {
646
+ return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
647
+ }, _typeof(o);
648
+ }
649
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPrimitive.js
650
+ function toPrimitive(t, r) {
651
+ if ("object" != _typeof(t) || !t) return t;
652
+ var e = t[Symbol.toPrimitive];
653
+ if (void 0 !== e) {
654
+ var i = e.call(t, r || "default");
655
+ if ("object" != _typeof(i)) return i;
656
+ throw new TypeError("@@toPrimitive must return a primitive value.");
657
+ }
658
+ return ("string" === r ? String : Number)(t);
659
+ }
660
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/toPropertyKey.js
661
+ function toPropertyKey(t) {
662
+ var i = toPrimitive(t, "string");
663
+ return "symbol" == _typeof(i) ? i : i + "";
664
+ }
665
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/defineProperty.js
666
+ function _defineProperty(e, r, t) {
667
+ return (r = toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
668
+ value: t,
669
+ enumerable: !0,
670
+ configurable: !0,
671
+ writable: !0
672
+ }) : e[r] = t, e;
673
+ }
674
+ // src/markitdown.ts
584
675
  var MarkItDown = class {
585
676
  constructor() {
586
- this.converters = [];
587
- this.registerConverter(new PlainTextConverter());
588
- this.registerConverter(new HtmlConverter());
589
- this.registerConverter(new ZipConverter());
590
- this.registerConverter(new RSSConverter());
591
- this.registerConverter(new WikipediaConverter());
592
- this.registerConverter(new YouTubeConverter());
593
- this.registerConverter(new BingSerpConverter());
594
- this.registerConverter(new DocxConverter());
595
- this.registerConverter(new XlsxConverter());
596
- this.registerConverter(new PptxConverter());
597
- this.registerConverter(new PdfConverter());
598
- this.registerConverter(new IpynbConverter());
677
+ _defineProperty(this, "converters", []);
678
+ const ordered = [
679
+ PlainTextConverter,
680
+ HtmlConverter,
681
+ ZipConverter,
682
+ RSSConverter,
683
+ WikipediaConverter,
684
+ YouTubeConverter,
685
+ BingSerpConverter,
686
+ DocxConverter,
687
+ XlsxConverter,
688
+ PptxConverter,
689
+ PdfConverter,
690
+ IpynbConverter
691
+ ];
692
+ for (const converter of ordered) this.registerConverter(new converter());
599
693
  }
600
694
  /** Convert a URL, response, blob, array buffer, data view, or typed array. */
601
695
  async convert(source, options = {}) {
602
696
  if (typeof source === "string" || source instanceof URL) return this.convertUrl(source.toString(), options);
603
697
  if (isResponse(source)) return this.convertResponse(source, options);
604
- const extensions = /* @__PURE__ */ new Set();
605
- this.addExtension(extensions, options.file_extension?.replace(/^\./, ""));
606
- if (isBlob(source)) {
607
- this.addExtension(extensions, extensionFromMime(source.type));
608
- if ("name" in source && typeof source.name === "string") this.addExtension(extensions, extensionFromPathname(source.name));
609
- }
610
- if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` when converting bytes.");
611
- return this.convertBytes(await toUint8Array(source), extensions, options);
612
- }
613
- /**
614
- * @deprecated Pass the byte source directly to `convert`.
615
- */
616
- async convertBuffer(source, options) {
617
- return this.convert(source, options);
698
+ const bytes = await toUint8Array(source);
699
+ const extensions = await detectExtensions(bytes, {
700
+ extension: options.extension,
701
+ filename: isBlob(source) && "name" in source ? String(source.name) : null,
702
+ urls: [options.url],
703
+ contentType: isBlob(source) ? source.type : null
704
+ });
705
+ return this.convertBytes(bytes, extensions, options);
618
706
  }
619
707
  async convertUrl(source, { fetch: fetchImplementation = globalThis.fetch, ...options }) {
620
708
  const url = new URL(source);
@@ -628,44 +716,39 @@ var MarkItDown = class {
628
716
  });
629
717
  }
630
718
  async convertResponse(response, options) {
631
- const extensions = /* @__PURE__ */ new Set();
632
- this.addExtension(extensions, options.file_extension);
633
- this.addExtension(extensions, extensionFromMime(response.headers.get("content-type")));
634
- const filename = (response.headers.get("content-disposition") ?? "").match(/filename\*?=(?:UTF-8''|["']?)([^;"']+)/i)?.[1];
635
- if (filename) this.addExtension(extensions, extensionFromPathname(decodeURIComponent(filename.trim())));
636
- const responseUrl = options.url || response.url;
637
- if (responseUrl) this.addExtension(extensions, extensionFromPathname(new URL(responseUrl).pathname));
638
- if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` or a Content-Type header.");
639
- return this.convertBytes(new Uint8Array(await response.arrayBuffer()), extensions, {
719
+ const bytes = new Uint8Array(await response.arrayBuffer());
720
+ const extensions = await detectExtensions(bytes, {
721
+ extension: options.extension,
722
+ filename: filenameFromDisposition(response.headers.get("content-disposition")),
723
+ urls: [options.url, response.url],
724
+ contentType: response.headers.get("content-type")
725
+ });
726
+ return this.convertBytes(bytes, extensions, {
640
727
  ...options,
641
- url: responseUrl
728
+ url: options.url || response.url || void 0
642
729
  });
643
730
  }
644
731
  async convertBytes(source, extensions, options) {
732
+ if (extensions.length === 0) throw new Error("Could not determine the file type. Provide `extension` for better results.");
645
733
  let lastError;
646
734
  for (const extension of extensions) for (const converter of this.converters) try {
647
735
  const result = await converter.convert(source, {
648
736
  ...options,
649
- file_extension: extension,
650
- _parent_converters: this.converters
737
+ extension,
738
+ _converters: this.converters
651
739
  });
652
740
  if (result) {
653
741
  result.markdown = result.markdown.replace(/\r\n|\r|\n/g, "\n").trim().replace(/\n{3,}/g, "\n\n");
654
- result.text_content = result.markdown;
655
742
  return result;
656
743
  }
657
744
  } catch (error) {
658
745
  if (options.debug) console.error("Converter threw error:", error);
659
746
  lastError = error;
660
747
  }
661
- const formats = [...extensions].join(", ");
748
+ const formats = extensions.join(", ");
662
749
  if (lastError) throw new Error(`Could not convert ${formats} to Markdown: ${String(lastError)}`);
663
750
  throw new Error(`Unsupported file type: ${formats}.`);
664
751
  }
665
- addExtension(extensions, extension) {
666
- if (!extension) return;
667
- extensions.add(extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`);
668
- }
669
752
  registerConverter(converter) {
670
753
  this.converters.unshift(converter);
671
754
  }
@@ -673,7 +756,6 @@ var MarkItDown = class {
673
756
  function isResponse(value) {
674
757
  return typeof Response !== "undefined" && value instanceof Response;
675
758
  }
676
- //#endregion
677
759
  export { MarkItDown };
678
760
 
679
761
  //# sourceMappingURL=index.js.map