@statewalker/content-extractors 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/collect-bytes.d.ts +16 -0
- package/dist/collect-bytes.d.ts.map +1 -0
- package/dist/collect-bytes.js +37 -0
- package/dist/collect-bytes.js.map +1 -0
- package/dist/extractor-registry.d.ts +38 -0
- package/dist/extractor-registry.d.ts.map +1 -0
- package/dist/extractor-registry.js +61 -0
- package/dist/extractor-registry.js.map +1 -0
- package/dist/extractors/docx-extractor.d.ts +11 -0
- package/dist/extractors/docx-extractor.d.ts.map +1 -0
- package/dist/extractors/docx-extractor.js +23 -0
- package/dist/extractors/docx-extractor.js.map +1 -0
- package/dist/extractors/html-extractor.d.ts +10 -0
- package/dist/extractors/html-extractor.d.ts.map +1 -0
- package/dist/extractors/html-extractor.js +16 -0
- package/dist/extractors/html-extractor.js.map +1 -0
- package/dist/extractors/index.d.ts +18 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/index.js +31 -0
- package/dist/extractors/index.js.map +1 -0
- package/dist/extractors/markdown-extractor.d.ts +11 -0
- package/dist/extractors/markdown-extractor.d.ts.map +1 -0
- package/dist/extractors/markdown-extractor.js +15 -0
- package/dist/extractors/markdown-extractor.js.map +1 -0
- package/dist/extractors/pdf-extractor.d.ts +10 -0
- package/dist/extractors/pdf-extractor.d.ts.map +1 -0
- package/dist/extractors/pdf-extractor.js +22 -0
- package/dist/extractors/pdf-extractor.js.map +1 -0
- package/dist/extractors/text-extractor.d.ts +10 -0
- package/dist/extractors/text-extractor.d.ts.map +1 -0
- package/dist/extractors/text-extractor.js +14 -0
- package/dist/extractors/text-extractor.js.map +1 -0
- package/dist/extractors/xlsx-extractor.d.ts +11 -0
- package/dist/extractors/xlsx-extractor.d.ts.map +1 -0
- package/dist/extractors/xlsx-extractor.js +49 -0
- package/dist/extractors/xlsx-extractor.js.map +1 -0
- package/dist/html-to-markdown.d.ts +10 -0
- package/dist/html-to-markdown.d.ts.map +1 -0
- package/dist/html-to-markdown.js +25 -0
- package/dist/html-to-markdown.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/markdown-to-html.d.ts +10 -0
- package/dist/markdown-to-html.d.ts.map +1 -0
- package/dist/markdown-to-html.js +22 -0
- package/dist/markdown-to-html.js.map +1 -0
- package/dist/mime-utils.d.ts +10 -0
- package/dist/mime-utils.d.ts.map +1 -0
- package/dist/mime-utils.js +29 -0
- package/dist/mime-utils.js.map +1 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +71 -0
- package/src/collect-bytes.ts +39 -0
- package/src/extractor-registry.ts +69 -0
- package/src/extractors/docx-extractor.ts +21 -0
- package/src/extractors/html-extractor.ts +13 -0
- package/src/extractors/index.ts +34 -0
- package/src/extractors/markdown-extractor.ts +12 -0
- package/src/extractors/pdf-extractor.ts +23 -0
- package/src/extractors/text-extractor.ts +11 -0
- package/src/extractors/xlsx-extractor.ts +57 -0
- package/src/html-to-markdown.ts +22 -0
- package/src/index.ts +7 -0
- package/src/markdown-to-html.ts +21 -0
- package/src/mime-utils.ts +25 -0
- package/src/turndown-plugin-gfm.d.ts +7 -0
- package/src/types.ts +24 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { collectBytes, collectText } from "./collect-bytes.js";
|
|
2
|
+
import { htmlToMarkdown } from "./html-to-markdown.js";
|
|
3
|
+
import { docxExtractor } from "./extractors/docx-extractor.js";
|
|
4
|
+
import { htmlExtractor } from "./extractors/html-extractor.js";
|
|
5
|
+
import { markdownExtractor } from "./extractors/markdown-extractor.js";
|
|
6
|
+
import { pdfExtractor } from "./extractors/pdf-extractor.js";
|
|
7
|
+
import { textExtractor } from "./extractors/text-extractor.js";
|
|
8
|
+
import { xlsxExtractor } from "./extractors/xlsx-extractor.js";
|
|
9
|
+
import { ExtractorRegistry } from "./extractor-registry.js";
|
|
10
|
+
import { createDefaultRegistry } from "./extractors/index.js";
|
|
11
|
+
import { markdownToHtml } from "./markdown-to-html.js";
|
|
12
|
+
import { getMimeType } from "./mime-utils.js";
|
|
13
|
+
export { ExtractorRegistry, collectBytes, collectText, createDefaultRegistry, docxExtractor, getMimeType, htmlExtractor, htmlToMarkdown, markdownExtractor, markdownToHtml, pdfExtractor, textExtractor, xlsxExtractor };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/markdown-to-html.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Provides the reverse of `htmlToMarkdown` for cases where extracted
|
|
4
|
+
* markdown needs to be rendered back to HTML (e.g. previews, email
|
|
5
|
+
* output). Configured with pass-through raw HTML and auto-linking
|
|
6
|
+
* so embedded markup and bare URLs survive the round-trip.
|
|
7
|
+
*/
|
|
8
|
+
export declare function markdownToHtml(markdown: string): string;
|
|
9
|
+
//#endregion
|
|
10
|
+
//# sourceMappingURL=markdown-to-html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-to-html.d.ts","names":[],"sources":["../src/markdown-to-html.ts"],"mappings":";;;;;;;wBAegB,eAAe"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
//#region src/markdown-to-html.ts
|
|
3
|
+
const md = new MarkdownIt({
|
|
4
|
+
html: true,
|
|
5
|
+
linkify: true,
|
|
6
|
+
typographer: true,
|
|
7
|
+
breaks: false
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Provides the reverse of `htmlToMarkdown` for cases where extracted
|
|
11
|
+
* markdown needs to be rendered back to HTML (e.g. previews, email
|
|
12
|
+
* output). Configured with pass-through raw HTML and auto-linking
|
|
13
|
+
* so embedded markup and bare URLs survive the round-trip.
|
|
14
|
+
*/
|
|
15
|
+
function markdownToHtml(markdown) {
|
|
16
|
+
if (!markdown || markdown.trim() === "") return "";
|
|
17
|
+
return md.render(markdown);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { markdownToHtml };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=markdown-to-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-to-html.js","names":[],"sources":["../src/markdown-to-html.ts"],"sourcesContent":["import MarkdownIt from \"markdown-it\";\n\nconst md = new MarkdownIt({\n html: true,\n linkify: true,\n typographer: true,\n breaks: false,\n});\n\n/**\n * Provides the reverse of `htmlToMarkdown` for cases where extracted\n * markdown needs to be rendered back to HTML (e.g. previews, email\n * output). Configured with pass-through raw HTML and auto-linking\n * so embedded markup and bare URLs survive the round-trip.\n */\nexport function markdownToHtml(markdown: string): string {\n if (!markdown || markdown.trim() === \"\") {\n return \"\";\n }\n return md.render(markdown);\n}\n"],"mappings":";;AAEA,MAAM,KAAK,IAAI,WAAW;CACxB,MAAM;CACN,SAAS;CACT,aAAa;CACb,QAAQ;AACV,CAAC;;;;;;;AAQD,SAAgB,eAAe,UAA0B;CACvD,IAAI,CAAC,YAAY,SAAS,KAAK,MAAM,IACnC,OAAO;CAET,OAAO,GAAG,OAAO,QAAQ;AAC3B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/mime-utils.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight alternative to a full MIME-sniffing library -- covers only
|
|
4
|
+
* the extensions this package can actually extract, keeping the dependency
|
|
5
|
+
* footprint small. Used by callers who have a filename but no
|
|
6
|
+
* Content-Type header and need a MIME type for the registry fallback path.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getMimeType(path: string): string | undefined;
|
|
9
|
+
//#endregion
|
|
10
|
+
//# sourceMappingURL=mime-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime-utils.d.ts","names":[],"sources":["../src/mime-utils.ts"],"mappings":";;;;;;;wBAmBgB,YAAY"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/mime-utils.ts
|
|
2
|
+
const MIME_MAP = {
|
|
3
|
+
".md": "text/markdown",
|
|
4
|
+
".markdown": "text/markdown",
|
|
5
|
+
".txt": "text/plain",
|
|
6
|
+
".html": "text/html",
|
|
7
|
+
".htm": "text/html",
|
|
8
|
+
".json": "application/json",
|
|
9
|
+
".pdf": "application/pdf",
|
|
10
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
11
|
+
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
|
12
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Lightweight alternative to a full MIME-sniffing library -- covers only
|
|
16
|
+
* the extensions this package can actually extract, keeping the dependency
|
|
17
|
+
* footprint small. Used by callers who have a filename but no
|
|
18
|
+
* Content-Type header and need a MIME type for the registry fallback path.
|
|
19
|
+
*/
|
|
20
|
+
function getMimeType(path) {
|
|
21
|
+
const dotIndex = path.lastIndexOf(".");
|
|
22
|
+
if (dotIndex === -1) return void 0;
|
|
23
|
+
const ext = path.slice(dotIndex).toLowerCase();
|
|
24
|
+
return MIME_MAP[ext];
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { getMimeType };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=mime-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime-utils.js","names":[],"sources":["../src/mime-utils.ts"],"sourcesContent":["const MIME_MAP: Record<string, string> = {\n \".md\": \"text/markdown\",\n \".markdown\": \"text/markdown\",\n \".txt\": \"text/plain\",\n \".html\": \"text/html\",\n \".htm\": \"text/html\",\n \".json\": \"application/json\",\n \".pdf\": \"application/pdf\",\n \".docx\": \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n \".docm\": \"application/vnd.ms-word.document.macroEnabled.12\",\n \".xlsx\": \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n};\n\n/**\n * Lightweight alternative to a full MIME-sniffing library -- covers only\n * the extensions this package can actually extract, keeping the dependency\n * footprint small. Used by callers who have a filename but no\n * Content-Type header and need a MIME type for the registry fallback path.\n */\nexport function getMimeType(path: string): string | undefined {\n const dotIndex = path.lastIndexOf(\".\");\n if (dotIndex === -1) return undefined;\n const ext = path.slice(dotIndex).toLowerCase();\n return MIME_MAP[ext];\n}\n"],"mappings":";AAAA,MAAM,WAAmC;CACvC,OAAO;CACP,aAAa;CACb,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,SAAS;CACT,SAAS;CACT,SAAS;AACX;;;;;;;AAQA,SAAgB,YAAY,MAAkC;CAC5D,MAAM,WAAW,KAAK,YAAY,GAAG;CACrC,IAAI,aAAa,IAAI,OAAO,KAAA;CAC5B,MAAM,MAAM,KAAK,MAAM,QAAQ,CAAC,CAAC,YAAY;CAC7C,OAAO,SAAS;AAClB"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The lowest-level extraction contract: raw bytes in, text out.
|
|
4
|
+
* Individual format handlers (PDF, DOCX, HTML, etc.) implement this type
|
|
5
|
+
* so the registry can treat every file format uniformly without knowing
|
|
6
|
+
* the parsing details.
|
|
7
|
+
*/
|
|
8
|
+
export type ContentExtractor = (content: AsyncIterable<Uint8Array> | Iterable<Uint8Array>) => Promise<string | unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Decouples post-processing from extraction so callers stay free of
|
|
11
|
+
* LLM or AI dependencies. Consumers can inject an LLM-backed normalizer
|
|
12
|
+
* to clean up, restructure, or enrich extracted markdown without the
|
|
13
|
+
* core package ever knowing about the LLM.
|
|
14
|
+
*/
|
|
15
|
+
export interface ContentNormalizer {
|
|
16
|
+
/**
|
|
17
|
+
* Refines raw extracted markdown -- fixing formatting artifacts, improving
|
|
18
|
+
* structure, or enriching content. The optional context hint lets the
|
|
19
|
+
* normalizer tailor its output (e.g. summarize for a specific audience).
|
|
20
|
+
*/
|
|
21
|
+
normalize(params: {
|
|
22
|
+
markdown: string;
|
|
23
|
+
context?: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
markdown: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;YAMY,oBACV,SAAS,cAAc,cAAc,SAAS,gBAC3C;;;;;;;iBAQY;;;;;;EAMf,UAAU;IAAU;IAAkB;MAAqB;IAAU"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@statewalker/content-extractors",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Content extractors: turn PDF, DOCX, XLSX, Markdown, and HTML into markdown text.",
|
|
7
|
+
"homepage": "https://github.com/statewalker/statewalker-content",
|
|
8
|
+
"author": {
|
|
9
|
+
"name": "Mikhail Kotelnikov",
|
|
10
|
+
"email": "mikhail.kotelnikov@gmail.com"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+ssh://git@github.com/statewalker/statewalker-content.git"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"source": "./src/types.ts",
|
|
26
|
+
"types": "./dist/types.d.ts",
|
|
27
|
+
"import": "./dist/types.js",
|
|
28
|
+
"default": "./dist/types.js"
|
|
29
|
+
},
|
|
30
|
+
"./extractors": {
|
|
31
|
+
"source": "./src/extractors/index.ts",
|
|
32
|
+
"types": "./dist/extractors/index.d.ts",
|
|
33
|
+
"import": "./dist/extractors/index.js",
|
|
34
|
+
"default": "./dist/extractors/index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"src"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@joplin/turndown-plugin-gfm": "^1.0.68",
|
|
43
|
+
"@types/turndown": "^5.0.6",
|
|
44
|
+
"exceljs": "^4.4.0",
|
|
45
|
+
"mammoth": "^1.13.0",
|
|
46
|
+
"markdown-it": "^15.0.2",
|
|
47
|
+
"turndown": "^7.2.4",
|
|
48
|
+
"unpdf": "^1.8.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^26.6.4",
|
|
52
|
+
"rimraf": "^6.1.3",
|
|
53
|
+
"tsdown": "^0.23.0",
|
|
54
|
+
"typescript": "^7.0.2",
|
|
55
|
+
"vitest": "^5.0.3"
|
|
56
|
+
},
|
|
57
|
+
"sideEffects": false,
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsdown",
|
|
63
|
+
"dev": "tsdown --watch",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"clean": "rimraf dist",
|
|
68
|
+
"lint": "biome check --write .",
|
|
69
|
+
"format": "biome format --write ."
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges the streaming world (chunked iterables from fetch, file reads,
|
|
3
|
+
* etc.) with libraries that require a single contiguous buffer (pdfjs-dist,
|
|
4
|
+
* mammoth). Returns the original chunk when only one arrives, avoiding
|
|
5
|
+
* an extra copy in the common single-read case.
|
|
6
|
+
*/
|
|
7
|
+
export async function collectBytes(
|
|
8
|
+
content: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
|
|
9
|
+
): Promise<Uint8Array> {
|
|
10
|
+
const chunks: Uint8Array[] = [];
|
|
11
|
+
let totalLength = 0;
|
|
12
|
+
for await (const chunk of content) {
|
|
13
|
+
chunks.push(chunk);
|
|
14
|
+
totalLength += chunk.length;
|
|
15
|
+
}
|
|
16
|
+
const first = chunks[0];
|
|
17
|
+
if (chunks.length === 1 && first) {
|
|
18
|
+
return first;
|
|
19
|
+
}
|
|
20
|
+
const result = new Uint8Array(totalLength);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) {
|
|
23
|
+
result.set(chunk, offset);
|
|
24
|
+
offset += chunk.length;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shorthand for text-based extractors (plain text, markdown, HTML) that
|
|
31
|
+
* need the full content as a string. Avoids duplicating the
|
|
32
|
+
* collect-then-decode pattern across every text extractor.
|
|
33
|
+
*/
|
|
34
|
+
export async function collectText(
|
|
35
|
+
content: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const bytes = await collectBytes(content);
|
|
38
|
+
return new TextDecoder().decode(bytes);
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ContentExtractor } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central routing table that decouples file-format knowledge from the
|
|
5
|
+
* extraction pipeline. New formats can be added without touching pipeline
|
|
6
|
+
* code. Longest-suffix matching ensures compound extensions (e.g.
|
|
7
|
+
* `*.tar.gz`) win over shorter ones, and MIME-type fallback handles
|
|
8
|
+
* files that arrive without a recognizable extension.
|
|
9
|
+
*/
|
|
10
|
+
export class ExtractorRegistry {
|
|
11
|
+
private patternMap = new Map<string, ContentExtractor>();
|
|
12
|
+
private mimeMap = new Map<string, ContentExtractor>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Associates a suffix pattern with an extractor so the registry can
|
|
16
|
+
* route files to the right handler based on their filename.
|
|
17
|
+
*/
|
|
18
|
+
registerByPattern(pattern: string, extractor: ContentExtractor): void {
|
|
19
|
+
this.patternMap.set(pattern, extractor);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Provides a fallback route for files that lack a recognizable extension
|
|
24
|
+
* (e.g. temp files or HTTP responses identified only by Content-Type).
|
|
25
|
+
*/
|
|
26
|
+
registerByMime(mimeType: string, extractor: ContentExtractor): void {
|
|
27
|
+
this.mimeMap.set(mimeType, extractor);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolves by filename suffix, preferring the most specific match so
|
|
32
|
+
* compound extensions like `*.test.html` beat `*.html` when both exist.
|
|
33
|
+
*/
|
|
34
|
+
getByPath(path: string): ContentExtractor | undefined {
|
|
35
|
+
const filename = path.includes("/") ? path.slice(path.lastIndexOf("/") + 1) : path;
|
|
36
|
+
const lowerFilename = filename.toLowerCase();
|
|
37
|
+
|
|
38
|
+
let bestMatch: ContentExtractor | undefined;
|
|
39
|
+
let bestLength = 0;
|
|
40
|
+
|
|
41
|
+
for (const [pattern, extractor] of this.patternMap) {
|
|
42
|
+
// Pattern format: *.ext or *.compound.ext
|
|
43
|
+
const suffix = pattern.startsWith("*") ? pattern.slice(1) : pattern;
|
|
44
|
+
if (lowerFilename.endsWith(suffix) && suffix.length > bestLength) {
|
|
45
|
+
bestMatch = extractor;
|
|
46
|
+
bestLength = suffix.length;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return bestMatch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Direct MIME lookup -- used internally by `get` as the fallback path. */
|
|
54
|
+
getByMime(mimeType: string): ContentExtractor | undefined {
|
|
55
|
+
return this.mimeMap.get(mimeType);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Single entry point for the pipeline -- tries the most reliable signal
|
|
60
|
+
* (file extension) first, then falls back to MIME type so callers never
|
|
61
|
+
* need to worry about the resolution strategy.
|
|
62
|
+
*/
|
|
63
|
+
get(path: string, mimeType?: string): ContentExtractor | undefined {
|
|
64
|
+
const byPath = this.getByPath(path);
|
|
65
|
+
if (byPath) return byPath;
|
|
66
|
+
if (mimeType) return this.getByMime(mimeType);
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import mammoth from "mammoth";
|
|
2
|
+
import { collectBytes } from "../collect-bytes.js";
|
|
3
|
+
import { htmlToMarkdown } from "../html-to-markdown.js";
|
|
4
|
+
import type { ContentExtractor } from "../types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Unlocks Word document content for text-based pipelines via a two-stage
|
|
8
|
+
* conversion (DOCX -> HTML -> markdown). Supplies both `buffer` and
|
|
9
|
+
* `arrayBuffer` to mammoth so the same code runs in Node.js and browsers
|
|
10
|
+
* without environment detection.
|
|
11
|
+
*/
|
|
12
|
+
export const docxExtractor: ContentExtractor = async (content) => {
|
|
13
|
+
const bytes = await collectBytes(content);
|
|
14
|
+
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
15
|
+
// mammoth uses "buffer" in Node.js and "arrayBuffer" in browsers
|
|
16
|
+
const result = await mammoth.convertToHtml({
|
|
17
|
+
buffer: arrayBuffer,
|
|
18
|
+
arrayBuffer,
|
|
19
|
+
} as Parameters<typeof mammoth.convertToHtml>[0]);
|
|
20
|
+
return htmlToMarkdown(result.value);
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { collectText } from "../collect-bytes.js";
|
|
2
|
+
import { htmlToMarkdown } from "../html-to-markdown.js";
|
|
3
|
+
import type { ContentExtractor } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts HTML files to markdown so the pipeline outputs a uniform
|
|
7
|
+
* text format. Delegates to `htmlToMarkdown` for the actual conversion,
|
|
8
|
+
* keeping this extractor focused on the bytes-to-string bridging step.
|
|
9
|
+
*/
|
|
10
|
+
export const htmlExtractor: ContentExtractor = async (content) => {
|
|
11
|
+
const html = await collectText(content);
|
|
12
|
+
return htmlToMarkdown(html);
|
|
13
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { docxExtractor } from "./docx-extractor.js";
|
|
2
|
+
export { htmlExtractor } from "./html-extractor.js";
|
|
3
|
+
export { markdownExtractor } from "./markdown-extractor.js";
|
|
4
|
+
export { pdfExtractor } from "./pdf-extractor.js";
|
|
5
|
+
export { textExtractor } from "./text-extractor.js";
|
|
6
|
+
export { xlsxExtractor } from "./xlsx-extractor.js";
|
|
7
|
+
|
|
8
|
+
import { ExtractorRegistry } from "../extractor-registry.js";
|
|
9
|
+
import { docxExtractor } from "./docx-extractor.js";
|
|
10
|
+
import { htmlExtractor } from "./html-extractor.js";
|
|
11
|
+
import { markdownExtractor } from "./markdown-extractor.js";
|
|
12
|
+
import { pdfExtractor } from "./pdf-extractor.js";
|
|
13
|
+
import { textExtractor } from "./text-extractor.js";
|
|
14
|
+
import { xlsxExtractor } from "./xlsx-extractor.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provides a batteries-included registry so most consumers can start
|
|
18
|
+
* extracting immediately without manual format registration. Custom
|
|
19
|
+
* registries are still supported for apps that need to override or
|
|
20
|
+
* limit the built-in set.
|
|
21
|
+
*/
|
|
22
|
+
export function createDefaultRegistry(): ExtractorRegistry {
|
|
23
|
+
const registry = new ExtractorRegistry();
|
|
24
|
+
registry.registerByPattern("*.md", markdownExtractor);
|
|
25
|
+
registry.registerByPattern("*.markdown", markdownExtractor);
|
|
26
|
+
registry.registerByPattern("*.txt", textExtractor);
|
|
27
|
+
registry.registerByPattern("*.html", htmlExtractor);
|
|
28
|
+
registry.registerByPattern("*.htm", htmlExtractor);
|
|
29
|
+
registry.registerByPattern("*.pdf", pdfExtractor);
|
|
30
|
+
registry.registerByPattern("*.docx", docxExtractor);
|
|
31
|
+
registry.registerByPattern("*.docm", docxExtractor);
|
|
32
|
+
registry.registerByPattern("*.xlsx", xlsxExtractor);
|
|
33
|
+
return registry;
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { collectText } from "../collect-bytes.js";
|
|
2
|
+
import type { ContentExtractor } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Passthrough extractor for markdown files. Functionally identical to
|
|
6
|
+
* `textExtractor` but registered under its own extension patterns so
|
|
7
|
+
* the registry can distinguish `.md` from `.txt` -- allowing future
|
|
8
|
+
* markdown-specific processing without breaking existing registrations.
|
|
9
|
+
*/
|
|
10
|
+
export const markdownExtractor: ContentExtractor = async (content) => {
|
|
11
|
+
return collectText(content);
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { extractText } from "unpdf";
|
|
2
|
+
import { collectBytes } from "../collect-bytes.js";
|
|
3
|
+
import type { ContentExtractor } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts PDF content page-by-page, formatting each page as a markdown
|
|
7
|
+
* section with a `## Page N` header. This preserves page boundaries in
|
|
8
|
+
* the indexed content, making search results more precise.
|
|
9
|
+
*/
|
|
10
|
+
export const pdfExtractor: ContentExtractor = async (content) => {
|
|
11
|
+
const bytes = await collectBytes(content);
|
|
12
|
+
const { text: pages } = await extractText(bytes);
|
|
13
|
+
|
|
14
|
+
const sections: string[] = [];
|
|
15
|
+
for (let i = 0; i < pages.length; i++) {
|
|
16
|
+
const pageText = pages[i]?.trim();
|
|
17
|
+
if (pageText) {
|
|
18
|
+
sections.push(`## Page ${i + 1}\n\n${pageText}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sections.join("\n\n");
|
|
23
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { collectText } from "../collect-bytes.js";
|
|
2
|
+
import type { ContentExtractor } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Identity extractor for plain text -- no conversion needed, just
|
|
6
|
+
* decode bytes to a string. Exists as a distinct entry so `.txt` files
|
|
7
|
+
* route through the same pipeline as every other format.
|
|
8
|
+
*/
|
|
9
|
+
export const textExtractor: ContentExtractor = async (content) => {
|
|
10
|
+
return collectText(content);
|
|
11
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import ExcelJS from "exceljs";
|
|
2
|
+
import { collectBytes } from "../collect-bytes.js";
|
|
3
|
+
import type { ContentExtractor } from "../types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts Excel spreadsheets into markdown tables so the content
|
|
7
|
+
* is searchable and consumable by text-based pipelines. Each worksheet
|
|
8
|
+
* becomes a separate section with a level-2 heading. Empty sheets are
|
|
9
|
+
* skipped to avoid noise.
|
|
10
|
+
*/
|
|
11
|
+
export const xlsxExtractor: ContentExtractor = async (content) => {
|
|
12
|
+
const bytes = await collectBytes(content);
|
|
13
|
+
const workbook = new ExcelJS.Workbook();
|
|
14
|
+
const arrayBuffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
15
|
+
await workbook.xlsx.load(arrayBuffer as ArrayBuffer);
|
|
16
|
+
|
|
17
|
+
const sections: string[] = [];
|
|
18
|
+
|
|
19
|
+
workbook.eachSheet((sheet) => {
|
|
20
|
+
const rows: string[][] = [];
|
|
21
|
+
|
|
22
|
+
sheet.eachRow((row) => {
|
|
23
|
+
const cells: string[] = [];
|
|
24
|
+
row.eachCell({ includeEmpty: true }, (cell) => {
|
|
25
|
+
try {
|
|
26
|
+
const v = cell.value;
|
|
27
|
+
cells.push(v == null ? "" : String(v));
|
|
28
|
+
} catch {
|
|
29
|
+
cells.push("");
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
rows.push(cells);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (rows.length === 0) return;
|
|
36
|
+
|
|
37
|
+
// Normalize column count to the widest row
|
|
38
|
+
const colCount = Math.max(...rows.map((r) => r.length));
|
|
39
|
+
const padded = rows.map((r) => {
|
|
40
|
+
while (r.length < colCount) r.push("");
|
|
41
|
+
return r;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// First row as headers, rest as data
|
|
45
|
+
const [headers, ...dataRows] = padded;
|
|
46
|
+
if (!headers) return;
|
|
47
|
+
|
|
48
|
+
const headerLine = `| ${headers.join(" | ")} |`;
|
|
49
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
50
|
+
const bodyLines = dataRows.map((r) => `| ${r.join(" | ")} |`);
|
|
51
|
+
|
|
52
|
+
const table = [headerLine, separator, ...bodyLines].join("\n");
|
|
53
|
+
sections.push(`## ${sheet.name}\n\n${table}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return sections.join("\n\n");
|
|
57
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference path="./turndown-plugin-gfm.d.ts" />
|
|
2
|
+
import { gfm } from "@joplin/turndown-plugin-gfm";
|
|
3
|
+
import TurndownService from "turndown";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes HTML into markdown so downstream consumers get a uniform
|
|
7
|
+
* text format regardless of whether the source was HTML, DOCX (via
|
|
8
|
+
* mammoth's HTML output), or any other HTML-producing extractor.
|
|
9
|
+
* Uses Turndown with GFM tables and strikethrough support.
|
|
10
|
+
*/
|
|
11
|
+
export function htmlToMarkdown(html: string): string {
|
|
12
|
+
const turndownService = new TurndownService({
|
|
13
|
+
headingStyle: "atx",
|
|
14
|
+
hr: "---",
|
|
15
|
+
bulletListMarker: "*",
|
|
16
|
+
codeBlockStyle: "fenced",
|
|
17
|
+
emDelimiter: "*",
|
|
18
|
+
strongDelimiter: "**",
|
|
19
|
+
});
|
|
20
|
+
turndownService.use(gfm);
|
|
21
|
+
return turndownService.turndown(html);
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
|
|
3
|
+
const md = new MarkdownIt({
|
|
4
|
+
html: true,
|
|
5
|
+
linkify: true,
|
|
6
|
+
typographer: true,
|
|
7
|
+
breaks: false,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provides the reverse of `htmlToMarkdown` for cases where extracted
|
|
12
|
+
* markdown needs to be rendered back to HTML (e.g. previews, email
|
|
13
|
+
* output). Configured with pass-through raw HTML and auto-linking
|
|
14
|
+
* so embedded markup and bare URLs survive the round-trip.
|
|
15
|
+
*/
|
|
16
|
+
export function markdownToHtml(markdown: string): string {
|
|
17
|
+
if (!markdown || markdown.trim() === "") {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
return md.render(markdown);
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const MIME_MAP: Record<string, string> = {
|
|
2
|
+
".md": "text/markdown",
|
|
3
|
+
".markdown": "text/markdown",
|
|
4
|
+
".txt": "text/plain",
|
|
5
|
+
".html": "text/html",
|
|
6
|
+
".htm": "text/html",
|
|
7
|
+
".json": "application/json",
|
|
8
|
+
".pdf": "application/pdf",
|
|
9
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
10
|
+
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
|
11
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Lightweight alternative to a full MIME-sniffing library -- covers only
|
|
16
|
+
* the extensions this package can actually extract, keeping the dependency
|
|
17
|
+
* footprint small. Used by callers who have a filename but no
|
|
18
|
+
* Content-Type header and need a MIME type for the registry fallback path.
|
|
19
|
+
*/
|
|
20
|
+
export function getMimeType(path: string): string | undefined {
|
|
21
|
+
const dotIndex = path.lastIndexOf(".");
|
|
22
|
+
if (dotIndex === -1) return undefined;
|
|
23
|
+
const ext = path.slice(dotIndex).toLowerCase();
|
|
24
|
+
return MIME_MAP[ext];
|
|
25
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare module "@joplin/turndown-plugin-gfm" {
|
|
2
|
+
import type TurndownService from "turndown";
|
|
3
|
+
export function gfm(service: TurndownService): void;
|
|
4
|
+
export function tables(service: TurndownService): void;
|
|
5
|
+
export function strikethrough(service: TurndownService): void;
|
|
6
|
+
export function taskListItems(service: TurndownService): void;
|
|
7
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lowest-level extraction contract: raw bytes in, text out.
|
|
3
|
+
* Individual format handlers (PDF, DOCX, HTML, etc.) implement this type
|
|
4
|
+
* so the registry can treat every file format uniformly without knowing
|
|
5
|
+
* the parsing details.
|
|
6
|
+
*/
|
|
7
|
+
export type ContentExtractor = (
|
|
8
|
+
content: AsyncIterable<Uint8Array> | Iterable<Uint8Array>,
|
|
9
|
+
) => Promise<string | unknown>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Decouples post-processing from extraction so callers stay free of
|
|
13
|
+
* LLM or AI dependencies. Consumers can inject an LLM-backed normalizer
|
|
14
|
+
* to clean up, restructure, or enrich extracted markdown without the
|
|
15
|
+
* core package ever knowing about the LLM.
|
|
16
|
+
*/
|
|
17
|
+
export interface ContentNormalizer {
|
|
18
|
+
/**
|
|
19
|
+
* Refines raw extracted markdown -- fixing formatting artifacts, improving
|
|
20
|
+
* structure, or enriching content. The optional context hint lets the
|
|
21
|
+
* normalizer tailor its output (e.g. summarize for a specific audience).
|
|
22
|
+
*/
|
|
23
|
+
normalize(params: { markdown: string; context?: string }): Promise<{ markdown: string }>;
|
|
24
|
+
}
|