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