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