@ryangarber/markitdown-ts 0.1.4 → 0.2.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/dist/index.js CHANGED
@@ -1,12 +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
- import ExcelJS from "exceljs";
7
- import { DOMParser } from "@xmldom/xmldom";
10
+ import { pptxToHtml } from "@jvmr/pptx-to-html";
11
+ import { DOMParser, Document, Element, Node } from "@xmldom/xmldom";
12
+ import readExcelFile from "read-excel-file/universal";
8
13
  import { unzipSync } from "fflate";
9
- //#region src/bytes.ts
14
+ // src/utils/bytes.ts
10
15
  async function toUint8Array(source) {
11
16
  if (source instanceof Uint8Array) return source;
12
17
  if (isBlob(source)) return new Uint8Array(await source.arrayBuffer());
@@ -22,12 +27,11 @@ function decodeText(source) {
22
27
  function isBlob(value) {
23
28
  return typeof Blob !== "undefined" && value instanceof Blob;
24
29
  }
25
- //#endregion
26
- //#region src/custom-turndown.ts
27
- var CustomTurnDown = class {
30
+ // src/utils/turndown.ts
31
+ var Turndown = class {
28
32
  convertSoup(document) {
29
33
  const service = new TurndownService({ headingStyle: "atx" });
30
- service.use(turndownPluginGfm.gfm);
34
+ service.use(TurndownPluginGfm);
31
35
  service.addRule("anchor tags", {
32
36
  filter: ["a"],
33
37
  replacement(content, node) {
@@ -62,48 +66,166 @@ var CustomTurnDown = class {
62
66
  return `![${alt}](${source}${title ? ` "${title}"` : ""})`;
63
67
  }
64
68
  });
65
- return service.turndown(document);
69
+ return service.turndown(serializeNode(document));
66
70
  }
67
71
  };
68
- //#endregion
69
- //#region src/html-document.ts
70
- function parseHtmlDocument(html) {
71
- 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));
72
110
  }
73
- //#endregion
74
- //#region src/converters/bingserp.ts
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/");
192
+ }
193
+ // src/converters/bingserp.ts
75
194
  const BING_SEARCH_URL = /^https:\/\/www\.bing\.com\/search\?q=/i;
76
195
  const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
77
196
  var BingSerpConverter = class {
78
197
  async convert(source, options = {}) {
79
- const extension = options.file_extension?.toLowerCase() ?? "";
80
- const url = options.url ?? "";
81
- if (extension !== ".html" && extension !== ".htm" || !BING_SEARCH_URL.test(url)) return null;
82
- const document = parseHtmlDocument(decodeText(source));
83
- const query = new URL(url).searchParams.get("q") ?? "";
84
- document.querySelectorAll(".tptt").forEach((element) => {
85
- if (element.textContent) element.textContent += " ";
86
- });
87
- document.querySelectorAll(".algoSlug_icon").forEach((element) => {
88
- element.remove();
89
- });
90
- const turndown = new CustomTurnDown();
91
- const markdown = `## A Bing search for '${query}' found the following results:\n\n${[...document.querySelectorAll(".b_algo")].map((result) => {
92
- result.querySelectorAll("a[href]").forEach((anchor) => {
93
- const href = anchor.getAttribute("href");
94
- if (!href) return;
95
- try {
96
- const encoded = new URL(href, url).searchParams.get("u");
97
- if (encoded) anchor.setAttribute("href", decodeBingUrl(encoded));
98
- } 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 += " ";
99
204
  });
100
- return turndown.convertSoup(result).split(/\n+/).map((line) => line.trim()).filter(Boolean).join("\n");
101
- }).join("\n\n")}`;
102
- return {
103
- title: document.title || null,
104
- markdown,
105
- text_content: markdown
106
- };
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
+ });
107
229
  }
108
230
  };
109
231
  function decodeBingUrl(encoded) {
@@ -123,52 +245,46 @@ function decodeBingUrl(encoded) {
123
245
  }
124
246
  return new TextDecoder().decode(new Uint8Array(bytes));
125
247
  }
126
- //#endregion
127
- //#region src/converters/html.ts
248
+ // src/converters/html.ts
128
249
  var HtmlConverter = class {
129
250
  async convert(source, options) {
130
- const extension = options.file_extension?.toLowerCase() ?? "";
131
- if (extension !== ".html" && extension !== ".htm") return null;
251
+ if (options.extension !== "html" && options.extension !== "htm") return null;
132
252
  return this.convertHtml(decodeText(source));
133
253
  }
134
254
  async convertHtml(html) {
135
- const document = parseHtmlDocument(html);
136
- const title = document.title;
137
- document.querySelectorAll("script, style, title").forEach((element) => {
138
- 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
+ };
139
270
  });
140
- const markdown = new CustomTurnDown().convertSoup(document);
141
- return {
142
- title: title || null,
143
- markdown,
144
- text_content: markdown
145
- };
146
271
  }
147
272
  };
148
- //#endregion
149
- //#region src/converters/docx.ts
273
+ // src/converters/docx.ts
150
274
  var DocxConverter = class extends HtmlConverter {
151
275
  async convert(source, options) {
152
- if (options.file_extension?.toLowerCase() !== ".docx") return null;
276
+ if (options.extension !== "docx") return null;
153
277
  let mammothInput = { arrayBuffer: toArrayBuffer(source) };
154
278
  if (typeof process !== "undefined" && process.versions !== null && process.versions.node !== null) mammothInput = { buffer: Buffer.from(source) };
155
- const mammothOptions = {
156
- styleMap: options.styleMap,
157
- includeEmbeddedStyleMap: options.includeEmbeddedStyleMap,
158
- includeDefaultStyleMap: options.includeDefaultStyleMap,
159
- ignoreEmptyParagraphs: options.ignoreEmptyParagraphs,
160
- idPrefix: options.idPrefix
161
- };
162
- if (options.transformDocument) mammothOptions.transformDocument = options.transformDocument;
279
+ const mammothOptions = Object.fromEntries(Object.entries(options.mammoth ?? {}).flatMap(([key, value]) => value !== void 0 ? [[key, value]] : []));
163
280
  const html = await mammoth.convertToHtml(mammothInput, mammothOptions);
164
281
  return this.convertHtml(html.value);
165
282
  }
166
283
  };
167
- //#endregion
168
- //#region src/converters/ipynb.ts
284
+ // src/converters/ipynb.ts
169
285
  var IpynbConverter = class {
170
286
  async convert(source, options = {}) {
171
- if (options.file_extension?.toLowerCase() !== ".ipynb") return null;
287
+ if (options.extension !== "ipynb") return null;
172
288
  const notebook = JSON.parse(decodeText(source));
173
289
  const output = [];
174
290
  let title = notebook.metadata?.title ?? null;
@@ -183,267 +299,290 @@ var IpynbConverter = class {
183
299
  const markdown = output.join("\n\n");
184
300
  return {
185
301
  title,
186
- markdown,
187
- text_content: markdown
302
+ markdown
188
303
  };
189
304
  }
190
305
  };
191
- //#endregion
192
- //#region src/converters/pdf.ts
306
+ // src/converters/pdf.ts
193
307
  var PdfConverter = class {
194
308
  async convert(source, options) {
195
- if (options.file_extension?.toLowerCase() !== ".pdf") return null;
309
+ if (options.extension !== "pdf") return null;
196
310
  const pdf = await getDocumentProxy(source.slice());
197
311
  try {
198
312
  const [{ text }, { info }] = await Promise.all([extractText(pdf, { mergePages: true }), getMeta(pdf)]);
199
313
  return {
200
314
  title: typeof info.Title === "string" && info.Title.trim() ? info.Title : null,
201
- markdown: text,
202
- text_content: text
315
+ markdown: text
203
316
  };
204
317
  } finally {
205
318
  await pdf.loadingTask.destroy();
206
319
  }
207
320
  }
208
321
  };
209
- //#endregion
210
- //#region src/mime.ts
211
- const MIME_EXTENSIONS = {
212
- "application/atom+xml": ".atom",
213
- "application/json": ".json",
214
- "application/pdf": ".pdf",
215
- "application/zip": ".zip",
216
- "application/rss+xml": ".rss",
217
- "application/x-pdf": ".pdf",
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
322
+ // src/converters/plain-text.ts
251
323
  var PlainTextConverter = class {
252
324
  async convert(source, options = {}) {
253
- if (!isTextExtension(options.file_extension ?? "")) return null;
254
- const content = decodeText(source);
325
+ if (!isTextExtension(options.extension ?? "")) return null;
255
326
  return {
256
327
  title: null,
257
- markdown: content,
258
- text_content: content
328
+ markdown: decodeText(source)
259
329
  };
260
330
  }
261
331
  };
262
- //#endregion
263
- //#region src/converters/wikipedia.ts
332
+ // src/converters/pptx.ts
333
+ var PptxConverter = class extends HtmlConverter {
334
+ async convert(source, options) {
335
+ if (options.extension !== "pptx") return null;
336
+ const slides = await pptxToHtml(toArrayBuffer(source), { domParserFactory: createPptxDomParser });
337
+ const sections = [];
338
+ for (const [index, html] of slides.entries()) {
339
+ const result = await this.convertHtml(`<html><body>${html}</body></html>`);
340
+ sections.push(`<!-- Slide number: ${index + 1} -->\n${result?.markdown.trim() ?? ""}`);
341
+ }
342
+ return {
343
+ title: null,
344
+ markdown: sections.join("\n\n")
345
+ };
346
+ }
347
+ };
348
+ function createPptxDomParser() {
349
+ if (typeof globalThis.DOMParser === "function") return new globalThis.DOMParser();
350
+ patchXmlDom();
351
+ return new DOMParser();
352
+ }
353
+ let xmlDomPatched = false;
354
+ function patchXmlDom() {
355
+ if (xmlDomPatched) return;
356
+ xmlDomPatched = true;
357
+ defineMethod(Element.prototype, "querySelector", querySelector);
358
+ defineMethod(Element.prototype, "querySelectorAll", querySelectorAll);
359
+ defineMethod(Document.prototype, "querySelector", querySelector);
360
+ defineMethod(Document.prototype, "querySelectorAll", querySelectorAll);
361
+ defineGetter(Element.prototype, "children", function() {
362
+ return [...this.childNodes].filter((node) => node.nodeType === 1);
363
+ });
364
+ defineGetter(Node.prototype, "parentElement", function() {
365
+ const parent = this.parentNode;
366
+ return parent?.nodeType === 1 ? parent : null;
367
+ });
368
+ }
369
+ function querySelector(selector) {
370
+ for (const group of selector.split(",")) {
371
+ const match = queryByPath(this, group.trim());
372
+ if (match) return match;
373
+ }
374
+ return null;
375
+ }
376
+ function querySelectorAll() {
377
+ return [];
378
+ }
379
+ function queryByPath(root, selector) {
380
+ const tokens = selector.split(/\s+/).filter(Boolean);
381
+ let current = [root];
382
+ for (const token of tokens) {
383
+ const localName = selectorLocalName(token);
384
+ const next = [];
385
+ for (const node of current) next.push(...Array.from(node.getElementsByTagNameNS("*", localName)));
386
+ current = next;
387
+ }
388
+ return current[0] ?? null;
389
+ }
390
+ function selectorLocalName(token) {
391
+ if (token.includes("|")) return token.slice(token.lastIndexOf("|") + 1);
392
+ return token.replace(/\\:/g, ":").split(":").pop() ?? token;
393
+ }
394
+ function defineMethod(target, name, value) {
395
+ Object.defineProperty(target, name, {
396
+ configurable: true,
397
+ writable: true,
398
+ value
399
+ });
400
+ }
401
+ function defineGetter(target, name, get) {
402
+ if (Object.getOwnPropertyDescriptor(target, name)) return;
403
+ Object.defineProperty(target, name, {
404
+ configurable: true,
405
+ get
406
+ });
407
+ }
408
+ // src/converters/wikipedia.ts
264
409
  const WIKIPEDIA_URL = /^https?:\/\/[a-z]{2,3}\.wikipedia\.org\//i;
265
410
  var WikipediaConverter = class {
266
411
  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();
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
+ };
272
428
  });
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
429
  }
282
430
  };
283
- //#endregion
284
- //#region src/converters/xlsx.ts
285
- const { Workbook } = ExcelJS;
431
+ // src/converters/xlsx.ts
286
432
  var XlsxConverter = class extends HtmlConverter {
287
433
  async convert(source, options) {
288
- if (options.file_extension?.toLowerCase() !== ".xlsx") return null;
289
- const workbook = new Workbook();
290
- await workbook.xlsx.load(toArrayBuffer(source));
434
+ if (options.extension !== "xlsx") return null;
435
+ const sheets = await readExcelFile(toArrayBuffer(source));
291
436
  const sections = [];
292
- for (const worksheet of workbook.worksheets) {
293
- const html = worksheetToHtml(worksheet);
437
+ for (const { sheet, data } of sheets) {
438
+ const html = sheetToHtml(data);
294
439
  if (!html) continue;
295
440
  const result = await this.convertHtml(html);
296
- sections.push(`## ${worksheet.name}\n${result?.markdown.trim() ?? ""}`);
441
+ sections.push(`## ${sheet}\n${result?.markdown.trim() ?? ""}`);
297
442
  }
298
- const markdown = sections.join("\n\n");
299
443
  return {
300
- title: workbook.title || "Untitled",
301
- markdown,
302
- text_content: markdown
444
+ title: null,
445
+ markdown: sections.join("\n\n")
303
446
  };
304
447
  }
305
448
  };
306
- function worksheetToHtml(worksheet) {
307
- const columnCount = worksheet.columnCount;
308
- if (!columnCount || !worksheet.actualRowCount) return "";
309
- const rows = [];
310
- worksheet.eachRow({ includeEmpty: true }, (row) => {
449
+ function sheetToHtml(rows) {
450
+ const columnCount = rows.reduce((max, row) => Math.max(max, row.length), 0);
451
+ if (!columnCount || rows.length === 0) return "";
452
+ return `<html><body><table>${rows.map((row) => {
311
453
  const cells = [];
312
- for (let column = 1; column <= columnCount; column++) cells.push(`<td>${escapeHtml(cellText(row.getCell(column)))}</td>`);
313
- rows.push(`<tr>${cells.join("")}</tr>`);
314
- });
315
- return `<html><body><table>${rows.join("")}</table></body></html>`;
454
+ for (let column = 0; column < columnCount; column++) cells.push(`<td>${escapeHtml(cellText(row[column]))}</td>`);
455
+ return `<tr>${cells.join("")}</tr>`;
456
+ }).join("")}</table></body></html>`;
316
457
  }
317
- function cellText(cell) {
318
- if (cell.isMerged && cell.master !== cell) return "";
319
- return cell.text;
458
+ function cellText(value) {
459
+ if (value == null) return "";
460
+ if (value instanceof Date) return value.toISOString();
461
+ return String(value);
320
462
  }
321
463
  function escapeHtml(value) {
322
464
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
323
465
  }
324
- //#endregion
325
- //#region src/converters/xml-rss-atom.ts
466
+ // src/converters/xml-rss-atom.ts
326
467
  var RSSConverter = class {
327
468
  async convert(source, options = {}) {
328
- const extension = options.file_extension?.toLowerCase() ?? "";
329
- if (![
330
- ".xml",
331
- ".rss",
332
- ".atom"
333
- ].includes(extension)) return null;
469
+ if (options.extension !== "xml" && options.extension !== "rss" && options.extension !== "atom") return null;
334
470
  const document = new DOMParser().parseFromString(decodeText(source), "text/xml");
335
- if (document.getElementsByTagName("rss").length > 0) return this.parseRss(document);
336
- 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);
337
473
  return null;
338
474
  }
339
- parseAtom(document) {
340
- const root = document.getElementsByTagName("feed")[0];
341
- if (!root) return null;
342
- const title = getText(root, "title");
343
- const subtitle = getText(root, "subtitle");
344
- const sections = [`# ${title ?? "Feed"}`];
345
- if (subtitle) sections.push(subtitle);
346
- for (const entry of [...root.getElementsByTagName("entry")]) {
347
- const entryTitle = getText(entry, "title");
348
- const updated = getText(entry, "updated");
349
- const summary = getText(entry, "summary");
350
- const content = getText(entry, "content");
351
- const lines = [];
352
- if (entryTitle) lines.push(`## ${entryTitle}`);
353
- if (updated) lines.push(`Updated on: ${updated}`);
354
- if (summary) lines.push(parseContent(summary));
355
- if (content) lines.push(parseContent(content));
356
- sections.push(lines.join("\n"));
357
- }
358
- const markdown = sections.join("\n\n");
359
- return {
360
- title,
361
- markdown,
362
- text_content: markdown
363
- };
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"));
364
497
  }
365
- parseRss(document) {
366
- const channel = document.getElementsByTagName("rss")[0]?.getElementsByTagName("channel")[0];
367
- if (!channel) return null;
368
- const title = getText(channel, "title");
369
- const description = getText(channel, "description");
370
- const sections = title ? [`# ${title}`] : [];
371
- if (description) sections.push(description);
372
- for (const item of [...channel.getElementsByTagName("item")]) {
373
- const itemTitle = getText(item, "title");
374
- const published = getText(item, "pubDate");
375
- const itemDescription = getText(item, "description");
376
- const content = getText(item, "content:encoded");
377
- const lines = [];
378
- if (itemTitle) lines.push(`## ${itemTitle}`);
379
- if (published) lines.push(`Published on: ${published}`);
380
- if (itemDescription) lines.push(parseContent(itemDescription));
381
- if (content) lines.push(parseContent(content));
382
- sections.push(lines.join("\n"));
383
- }
384
- const markdown = sections.join("\n\n");
385
- return {
386
- title,
387
- markdown,
388
- text_content: markdown
389
- };
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"));
390
524
  }
391
- };
392
- function parseContent(content) {
393
- return new CustomTurnDown().convertSoup(parseHtmlDocument(content).documentElement);
525
+ return {
526
+ title,
527
+ markdown: sections.join("\n\n")
528
+ };
394
529
  }
395
530
  function getText(element, tagName) {
396
531
  return element.getElementsByTagName(tagName)[0]?.textContent?.trim() || null;
397
532
  }
398
- //#endregion
399
- //#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;
400
535
  var YouTubeConverter = class {
401
536
  async convert(source, options = {}) {
402
- const extension = options.file_extension?.toLowerCase() ?? "";
403
- if (extension !== ".html" && extension !== ".htm" || !options.url?.startsWith("https://www.youtube.com/watch?")) return null;
404
- const document = parseHtmlDocument(decodeText(source));
405
- const metadata = { title: document.title };
406
- document.querySelectorAll("meta").forEach((meta) => {
407
- const content = meta.getAttribute("content");
408
- const key = meta.getAttribute("itemprop") ?? meta.getAttribute("property") ?? meta.getAttribute("name");
409
- if (key && content) metadata[key] = content;
410
- });
411
- try {
412
- for (const script of document.querySelectorAll("script")) {
413
- const content = script.textContent ?? "";
414
- if (!content.includes("ytInitialData")) continue;
415
- const start = content.indexOf("{");
416
- const end = content.lastIndexOf("}");
417
- if (start >= 0 && end > start) {
418
- const description = findKey(JSON.parse(content.slice(start, end + 1)), "attributedDescriptionBodyText");
419
- 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;
420
556
  }
421
- break;
422
- }
423
- } catch {}
424
- const title = first(metadata, [
425
- "title",
426
- "og:title",
427
- "name"
428
- ]) || document.title || null;
429
- const lines = ["# YouTube"];
430
- if (title) lines.push(`## ${title}`);
431
- const stats = [];
432
- const views = first(metadata, ["interactionCount"]);
433
- const keywords = first(metadata, ["keywords"]);
434
- const runtime = first(metadata, ["duration"]);
435
- if (views) stats.push(`- **Views:** ${views}`);
436
- if (keywords) stats.push(`- **Keywords:** ${keywords}`);
437
- if (runtime) stats.push(`- **Runtime:** ${runtime}`);
438
- if (stats.length > 0) lines.push(`### Video Metadata\n${stats.join("\n")}`);
439
- const description = first(metadata, ["description", "og:description"]);
440
- if (description) lines.push(`### Description\n${description}`);
441
- const markdown = lines.join("\n\n");
442
- return {
443
- title,
444
- markdown,
445
- text_content: markdown
446
- };
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
+ });
447
586
  }
448
587
  };
449
588
  function first(metadata, keys) {
@@ -466,23 +605,20 @@ function findKey(value, key) {
466
605
  function isRecord(value) {
467
606
  return typeof value === "object" && value !== null;
468
607
  }
469
- //#endregion
470
- //#region src/converters/zip.ts
608
+ // src/converters/zip.ts
471
609
  var ZipConverter = class {
472
610
  async convert(source, options = {}) {
473
- if (options.file_extension?.toLowerCase() !== ".zip") return null;
611
+ if (options.extension !== "zip") return null;
474
612
  const files = unzipSync(source, { filter: (file) => !file.name.endsWith("/") });
475
- const converters = options._parent_converters ?? [];
613
+ const converters = options._converters ?? [];
476
614
  const sections = [`Content from the zip file \`${options.url ?? "archive.zip"}\`:`];
477
615
  for (const [name, bytes] of Object.entries(files)) {
478
616
  const result = await convertEntry(bytes, name, converters, options);
479
617
  if (result) sections.push(`## File: ${name}\n\n${result.markdown}`);
480
618
  }
481
- const markdown = sections.join("\n\n");
482
619
  return {
483
620
  title: null,
484
- markdown,
485
- text_content: markdown
621
+ markdown: sections.join("\n\n")
486
622
  };
487
623
  }
488
624
  };
@@ -491,9 +627,9 @@ async function convertEntry(source, name, converters, options) {
491
627
  if (!extension) return null;
492
628
  const entryOptions = {
493
629
  ...options,
494
- file_extension: extension,
630
+ extension,
495
631
  url: void 0,
496
- _parent_converters: converters
632
+ _converters: converters
497
633
  };
498
634
  for (const converter of converters) try {
499
635
  const result = await converter.convert(source, entryOptions);
@@ -501,41 +637,72 @@ async function convertEntry(source, name, converters, options) {
501
637
  } catch {}
502
638
  return null;
503
639
  }
504
- //#endregion
505
- //#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
506
675
  var MarkItDown = class {
507
676
  constructor() {
508
- this.converters = [];
509
- this.registerConverter(new PlainTextConverter());
510
- this.registerConverter(new HtmlConverter());
511
- this.registerConverter(new ZipConverter());
512
- this.registerConverter(new RSSConverter());
513
- this.registerConverter(new WikipediaConverter());
514
- this.registerConverter(new YouTubeConverter());
515
- this.registerConverter(new BingSerpConverter());
516
- this.registerConverter(new DocxConverter());
517
- this.registerConverter(new XlsxConverter());
518
- this.registerConverter(new PdfConverter());
519
- 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());
520
693
  }
521
694
  /** Convert a URL, response, blob, array buffer, data view, or typed array. */
522
695
  async convert(source, options = {}) {
523
696
  if (typeof source === "string" || source instanceof URL) return this.convertUrl(source.toString(), options);
524
697
  if (isResponse(source)) return this.convertResponse(source, options);
525
- const extensions = /* @__PURE__ */ new Set();
526
- this.addExtension(extensions, options.file_extension?.replace(/^\./, ""));
527
- if (isBlob(source)) {
528
- this.addExtension(extensions, extensionFromMime(source.type));
529
- if ("name" in source && typeof source.name === "string") this.addExtension(extensions, extensionFromPathname(source.name));
530
- }
531
- if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` when converting bytes.");
532
- return this.convertBytes(await toUint8Array(source), extensions, options);
533
- }
534
- /**
535
- * @deprecated Pass the byte source directly to `convert`.
536
- */
537
- async convertBuffer(source, options) {
538
- 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);
539
706
  }
540
707
  async convertUrl(source, { fetch: fetchImplementation = globalThis.fetch, ...options }) {
541
708
  const url = new URL(source);
@@ -549,44 +716,39 @@ var MarkItDown = class {
549
716
  });
550
717
  }
551
718
  async convertResponse(response, options) {
552
- const extensions = /* @__PURE__ */ new Set();
553
- this.addExtension(extensions, options.file_extension);
554
- this.addExtension(extensions, extensionFromMime(response.headers.get("content-type")));
555
- const filename = (response.headers.get("content-disposition") ?? "").match(/filename\*?=(?:UTF-8''|["']?)([^;"']+)/i)?.[1];
556
- if (filename) this.addExtension(extensions, extensionFromPathname(decodeURIComponent(filename.trim())));
557
- const responseUrl = options.url || response.url;
558
- if (responseUrl) this.addExtension(extensions, extensionFromPathname(new URL(responseUrl).pathname));
559
- if (extensions.size === 0) throw new Error("Could not determine the file type. Provide `file_extension` or a Content-Type header.");
560
- 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, {
561
727
  ...options,
562
- url: responseUrl
728
+ url: options.url || response.url || void 0
563
729
  });
564
730
  }
565
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.");
566
733
  let lastError;
567
734
  for (const extension of extensions) for (const converter of this.converters) try {
568
735
  const result = await converter.convert(source, {
569
736
  ...options,
570
- file_extension: extension,
571
- _parent_converters: this.converters
737
+ extension,
738
+ _converters: this.converters
572
739
  });
573
740
  if (result) {
574
741
  result.markdown = result.markdown.replace(/\r\n|\r|\n/g, "\n").trim().replace(/\n{3,}/g, "\n\n");
575
- result.text_content = result.markdown;
576
742
  return result;
577
743
  }
578
744
  } catch (error) {
579
745
  if (options.debug) console.error("Converter threw error:", error);
580
746
  lastError = error;
581
747
  }
582
- const formats = [...extensions].join(", ");
748
+ const formats = extensions.join(", ");
583
749
  if (lastError) throw new Error(`Could not convert ${formats} to Markdown: ${String(lastError)}`);
584
750
  throw new Error(`Unsupported file type: ${formats}.`);
585
751
  }
586
- addExtension(extensions, extension) {
587
- if (!extension) return;
588
- extensions.add(extension.startsWith(".") ? extension.toLowerCase() : `.${extension.toLowerCase()}`);
589
- }
590
752
  registerConverter(converter) {
591
753
  this.converters.unshift(converter);
592
754
  }
@@ -594,7 +756,6 @@ var MarkItDown = class {
594
756
  function isResponse(value) {
595
757
  return typeof Response !== "undefined" && value instanceof Response;
596
758
  }
597
- //#endregion
598
759
  export { MarkItDown };
599
760
 
600
761
  //# sourceMappingURL=index.js.map