@happyvertical/documents 0.111.3 → 0.111.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/AGENT.md CHANGED
@@ -26,6 +26,6 @@ pnpm --filter @happyvertical/documents test
26
26
  ## Ecosystem Relationships
27
27
  - Provides: Multi-part document processing with support for PDF, HTML, and Markdown
28
28
  - Implements: none
29
- - Requires: @happyvertical/files, @happyvertical/utils, @happyvertical/ocr, @happyvertical/pdf, @happyvertical/spider
29
+ - Requires: @happyvertical/files, @happyvertical/utils, @happyvertical/ocr, @happyvertical/pdf, @happyvertical/spider, @kreuzberg/node
30
30
  - Stability: stable (Primary package surface is described as implemented and production-oriented.)
31
31
  <!-- END AGENT:GENERATED -->
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
 
5
- Document processing with hierarchical structure. Currently supports PDF documents with text extraction, automatic document management system detection (WordPress Download Manager, CivicWeb, DocuShare), and file caching. Uses `@happyvertical/spider` for web page analysis and `@happyvertical/pdf` for PDF text extraction.
5
+ Document processing with hierarchical structure. Supports PDF documents with text extraction, Word (`.docx`) and PowerPoint (`.pptx`) files read through the optional Kreuzberg engine, automatic document management system detection (WordPress Download Manager, CivicWeb, DocuShare), and file caching. Uses `@happyvertical/spider` for web page analysis and `@happyvertical/pdf` for PDF text extraction.
6
6
 
7
7
  ## Installation
8
8
 
@@ -100,6 +100,27 @@ Base document handler. Manages downloading, caching, and local file path resolut
100
100
 
101
101
  Implements `DocumentProcessor`. Extracts text from PDF files, validates PDF headers (detects HTML cache poisoning), and caches processed results.
102
102
 
103
+ ### `DOCXProcessor` and `PPTXProcessor`
104
+
105
+ Read Word and PowerPoint files through [Kreuzberg](https://www.npmjs.com/package/@kreuzberg/node) (`@kreuzberg/node`, an **optional** dependency; no LibreOffice). `fetchDocument` picks them from the `.docx` / `.pptx` extension or MIME type.
106
+
107
+ - **DOCX**: one `markdown` part per heading section, nested by heading level (text before the first heading is a leading part titled with the document title). Embedded images are attached to the first part.
108
+ - **PPTX**: one `text` part per slide, in order. `part.metadata.slide` is the 1-based slide number, `part.title` the first line of slide text, `part.notes` the speaker notes (absent when the slide has none) and `part.images` the pictures on that slide (`image.data` holds the bytes, `image.altText` the description).
109
+ - **Kreuzberg not installed**: `process()` resolves with `parts: []` and `document.unsupported = { code: 'dependency-missing', reason, dependency: '@kreuzberg/node' }` instead of throwing. Corrupt files still reject.
110
+
111
+ For bytes you already hold in memory use `extractOffice(source, 'docx' | 'pptx', { extractImages? })`, which takes a `Buffer`, `Uint8Array` or file path and returns `{ status: 'ok', format, title, parts, metadata }` or `{ status: 'unsupported', code, reason, dependency, format }`.
112
+
113
+ ```typescript
114
+ import { extractOffice } from '@happyvertical/documents';
115
+
116
+ const result = await extractOffice(bytes, 'pptx');
117
+ if (result.status === 'unsupported') {
118
+ console.warn(result.reason);
119
+ } else {
120
+ for (const slide of result.parts) console.log(slide.metadata?.slide, slide.content, slide.notes);
121
+ }
122
+ ```
123
+
103
124
  ### `getTitleFromUrl(url, defaultTitle?)`
104
125
 
105
126
  Extracts a human-readable title from a URL by parsing the filename, removing extensions, and decoding URL-encoded characters.
@@ -112,6 +133,7 @@ interface Document {
112
133
  type: string;
113
134
  parts: DocumentPart[];
114
135
  metadata?: Record<string, any>;
136
+ unsupported?: { code: 'dependency-missing'; reason: string; dependency?: string };
115
137
  }
116
138
 
117
139
  interface DocumentPart {
@@ -120,6 +142,7 @@ interface DocumentPart {
120
142
  content: string;
121
143
  type: 'text' | 'html' | 'markdown';
122
144
  images?: DocumentImage[];
145
+ notes?: string; // speaker notes (PPTX slides)
123
146
  metadata?: Record<string, any>;
124
147
  parts?: DocumentPart[];
125
148
  }
@@ -128,6 +151,7 @@ interface DocumentImage {
128
151
  id: string;
129
152
  url: string;
130
153
  localPath?: string;
154
+ data?: Uint8Array; // in-memory bytes (Office documents)
131
155
  altText?: string;
132
156
  ocrText?: string;
133
157
  position?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,YAAS,CAAC;AAO9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAgFnB;AAED,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../src/factory.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,YAAS,CAAC;AAW9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC,CAoFnB;AAED,eAAe,aAAa,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @happyvertical/documents - Document processing with multi-part structure
3
3
  *
4
- * Provides document processing for PDFs with support for:
4
+ * Provides document processing for PDF, Word (.docx) and PowerPoint (.pptx)
5
+ * (the latter two through the optional Kreuzberg engine) with support for:
5
6
  * - Hierarchical document parts
6
7
  * - Automatic format detection from URL or MIME type
7
8
  * - Document management system detection (WordPress, CivicWeb, DocuShare)
@@ -21,8 +22,10 @@
21
22
  */
22
23
  export { Document } from "./document.js";
23
24
  export { fetchDocument } from "./factory.js";
25
+ export type { ExtractOfficeOptions, OfficeExtraction, OfficeFormat, OfficeResult, OfficeUnsupported, } from "./processors/office.js";
26
+ export { DOCX_MIME, DOCXProcessor, extractOffice, PPTX_MIME, PPTXProcessor, } from "./processors/office.js";
24
27
  export { PDFProcessor } from "./processors/pdf.js";
25
- export type { Document as DocumentType, DocumentImage, DocumentPart, DocumentProcessor, FetchDocumentOptions, } from "./types.js";
28
+ export type { Document as DocumentType, DocumentImage, DocumentPart, DocumentProcessor, FetchDocumentOptions, UnsupportedReason, } from "./types.js";
26
29
  export { getTitleFromUrl } from "./utils.js";
27
30
  /** @internal */
28
31
  export declare const PACKAGE_VERSION_INITIALIZED = true;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAY,CAAC;AAEtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAW,CAAC;AAG1C,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAkB,CAAC;AAEhD,YAAY,EACV,QAAQ,IAAI,YAAY,EACxB,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,GACrB,MAAM,YAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAS,CAAC;AAE1C,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAY,CAAC;AAEtC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAW,CAAC;AAG1C,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,iBAAiB,GAClB,MAAM,wBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EACT,aAAa,EACb,aAAa,EACb,SAAS,EACT,aAAa,GACd,MAAM,wBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAkB,CAAC;AAEhD,YAAY,EACV,QAAQ,IAAI,YAAY,EACxB,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,YAAS,CAAC;AAEjB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAS,CAAC;AAE1C,gBAAgB;AAChB,eAAO,MAAM,2BAA2B,OAAO,CAAC"}
package/dist/index.js CHANGED
@@ -177,6 +177,281 @@ function getTitleFromUrl(url, defaultTitle = "Document") {
177
177
  }
178
178
  }
179
179
  //#endregion
180
+ //#region src/processors/office.ts
181
+ var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
182
+ var PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
183
+ var MIME = {
184
+ docx: DOCX_MIME,
185
+ pptx: PPTX_MIME
186
+ };
187
+ var KREUZBERG_PACKAGE = "@kreuzberg/node";
188
+ var kreuzbergPromise;
189
+ /** Lazily load the optional engine; resolves `null` when it is absent. */
190
+ function loadKreuzberg() {
191
+ kreuzbergPromise ??= import("@kreuzberg/node").then((mod) => mod, () => null);
192
+ return kreuzbergPromise;
193
+ }
194
+ var SLIDE_MARKER = (n) => `\n<<hv-slide:${n}>>\n`;
195
+ var SLIDE_MARKER_FORMAT = SLIDE_MARKER("{page_num}");
196
+ var SLIDE_MARKER_RE = /<<hv-slide:(\d+)>>/g;
197
+ var NOTES_RE = /(?:^|\n\n)Notes:\n\n([\s\S]*)$/;
198
+ function toImage(image, format) {
199
+ return {
200
+ id: randomUUID(),
201
+ url: `office-image:${format}/${image.imageIndex}`,
202
+ data: image.data,
203
+ altText: image.description ?? void 0,
204
+ position: image.imageIndex,
205
+ metadata: {
206
+ format: image.format,
207
+ width: image.width ?? void 0,
208
+ height: image.height ?? void 0,
209
+ ...image.pageNumber ? { slide: image.pageNumber } : {}
210
+ }
211
+ };
212
+ }
213
+ function splitSlides(content) {
214
+ const slides = /* @__PURE__ */ new Map();
215
+ const matches = [...content.matchAll(SLIDE_MARKER_RE)];
216
+ matches.forEach((match, i) => {
217
+ const start = (match.index ?? 0) + match[0].length;
218
+ const end = matches[i + 1]?.index ?? content.length;
219
+ slides.set(Number(match[1]), content.slice(start, end).trim());
220
+ });
221
+ return slides;
222
+ }
223
+ function buildSlideParts(content, images) {
224
+ const slides = splitSlides(content);
225
+ if (slides.size === 0) {
226
+ const text = content.trim();
227
+ return text ? [{
228
+ id: randomUUID(),
229
+ title: "Presentation",
230
+ content: text,
231
+ type: "text",
232
+ metadata: {
233
+ source: "pptx",
234
+ slidesSplit: false
235
+ }
236
+ }] : [];
237
+ }
238
+ return [...slides.entries()].map(([slide, raw]) => {
239
+ const notesMatch = NOTES_RE.exec(raw);
240
+ const text = (notesMatch ? raw.slice(0, notesMatch.index) : raw).trim();
241
+ const notes = notesMatch?.[1]?.trim();
242
+ const slideImages = images.filter((img) => img.pageNumber === slide).map((img) => toImage(img, "pptx"));
243
+ const part = {
244
+ id: randomUUID(),
245
+ title: text.split("\n").find((line) => line.trim())?.trim() || `Slide ${slide}`,
246
+ content: text,
247
+ type: "text",
248
+ metadata: {
249
+ source: "pptx",
250
+ slide
251
+ }
252
+ };
253
+ if (notes) part.notes = notes;
254
+ if (slideImages.length > 0) part.images = slideImages;
255
+ return part;
256
+ });
257
+ }
258
+ function buildDocxParts(markdown, fallbackTitle, images) {
259
+ const root = {
260
+ level: 0,
261
+ title: "",
262
+ lines: [],
263
+ children: []
264
+ };
265
+ const stack = [root];
266
+ let inFence = false;
267
+ for (const line of markdown.split("\n")) {
268
+ if (/^\s*```/.test(line)) inFence = !inFence;
269
+ const heading = inFence ? null : /^(#{1,6})\s+(.*\S)\s*$/.exec(line);
270
+ if (!heading) {
271
+ stack[stack.length - 1].lines.push(line);
272
+ continue;
273
+ }
274
+ const section = {
275
+ level: heading[1].length,
276
+ title: heading[2],
277
+ lines: [],
278
+ children: []
279
+ };
280
+ while (stack[stack.length - 1].level >= section.level) stack.pop();
281
+ stack[stack.length - 1].children.push(section);
282
+ stack.push(section);
283
+ }
284
+ const toPart = (section) => {
285
+ const part = {
286
+ id: randomUUID(),
287
+ title: section.title,
288
+ content: section.lines.join("\n").trim(),
289
+ type: "markdown",
290
+ metadata: {
291
+ source: "docx",
292
+ headingLevel: section.level
293
+ }
294
+ };
295
+ if (section.children.length > 0) part.parts = section.children.map(toPart);
296
+ return part;
297
+ };
298
+ const parts = [];
299
+ const lead = root.lines.join("\n").trim();
300
+ if (lead || root.children.length === 0) {
301
+ if (lead) parts.push({
302
+ id: randomUUID(),
303
+ title: fallbackTitle,
304
+ content: lead,
305
+ type: "markdown",
306
+ metadata: {
307
+ source: "docx",
308
+ headingLevel: 0
309
+ }
310
+ });
311
+ }
312
+ parts.push(...root.children.map(toPart));
313
+ if (images.length > 0) {
314
+ if (parts.length === 0) parts.push({
315
+ id: randomUUID(),
316
+ title: fallbackTitle,
317
+ content: "",
318
+ type: "markdown",
319
+ metadata: {
320
+ source: "docx",
321
+ headingLevel: 0
322
+ }
323
+ });
324
+ parts[0].images = images;
325
+ }
326
+ return parts;
327
+ }
328
+ /**
329
+ * Read a DOCX or PPTX from memory (or a path) through Kreuzberg.
330
+ *
331
+ * Returns `{ status: 'unsupported' }` -- never throws for that case -- when the
332
+ * optional `@kreuzberg/node` dependency is not installed. Corrupt or
333
+ * unreadable files reject with the engine's error.
334
+ *
335
+ * @param source - File bytes or a local file path
336
+ * @param format - `'docx'` or `'pptx'`
337
+ */
338
+ async function extractOffice(source, format, options = {}) {
339
+ const kreuzberg = await loadKreuzberg();
340
+ if (!kreuzberg) return {
341
+ status: "unsupported",
342
+ code: "dependency-missing",
343
+ format,
344
+ dependency: KREUZBERG_PACKAGE,
345
+ reason: `Reading .${format} files requires the optional ${KREUZBERG_PACKAGE} package, which is not installed.`
346
+ };
347
+ const bytes = typeof source === "string" ? await promises.readFile(source) : Buffer.isBuffer(source) ? source : Buffer.from(source);
348
+ const wantImages = options.extractImages !== false;
349
+ const config = { images: { extractImages: wantImages } };
350
+ if (format === "pptx") config.pages = {
351
+ extractPages: true,
352
+ insertPageMarkers: true,
353
+ markerFormat: SLIDE_MARKER_FORMAT
354
+ };
355
+ else config.outputFormat = "markdown";
356
+ const result = await kreuzberg.extractBytes(bytes, MIME[format], config);
357
+ const content = result.content ?? "";
358
+ const rawImages = wantImages ? result.images ?? [] : [];
359
+ const title = result.metadata?.title?.trim() || void 0;
360
+ const parts = format === "pptx" ? buildSlideParts(content, rawImages) : buildDocxParts(content, title ?? options.title ?? "Document", rawImages.map((img) => toImage(img, "docx")));
361
+ return {
362
+ status: "ok",
363
+ format,
364
+ title,
365
+ parts,
366
+ metadata: {
367
+ processor: format,
368
+ engine: "kreuzberg",
369
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
370
+ hasImages: rawImages.length > 0,
371
+ ...format === "pptx" ? { slideCount: parts.filter((p) => p.metadata?.slide !== void 0).length } : {},
372
+ ...title ? { title } : {}
373
+ }
374
+ };
375
+ }
376
+ var OfficeProcessor = class {
377
+ async process(url, options = {}) {
378
+ const baseDoc = await Document.create(url, {
379
+ ...options,
380
+ type: options.type ?? MIME[this.format]
381
+ });
382
+ const cacheKey = `${baseDoc.localPath}.processed_${this.format}`;
383
+ const wantImages = options.extractImages !== false;
384
+ const cached = wantImages ? null : await getCached(cacheKey);
385
+ if (cached) try {
386
+ const parsed = JSON.parse(cached);
387
+ return {
388
+ url: baseDoc.url.toString(),
389
+ type: baseDoc.type,
390
+ parts: parsed.parts,
391
+ metadata: parsed.metadata || {}
392
+ };
393
+ } catch (error) {
394
+ console.warn(`Cached ${this.label} data corrupted, reprocessing`, error);
395
+ }
396
+ const result = await extractOffice(baseDoc.localPath, this.format, {
397
+ extractImages: wantImages,
398
+ title: getTitleFromUrl(url, this.label)
399
+ });
400
+ if (result.status === "unsupported") return {
401
+ url: baseDoc.url.toString(),
402
+ type: baseDoc.type,
403
+ parts: [],
404
+ metadata: {
405
+ processor: this.format,
406
+ unsupported: true
407
+ },
408
+ unsupported: {
409
+ code: result.code,
410
+ reason: result.reason,
411
+ dependency: result.dependency
412
+ }
413
+ };
414
+ const document = {
415
+ url: baseDoc.url.toString(),
416
+ type: baseDoc.type,
417
+ parts: result.parts,
418
+ metadata: result.metadata
419
+ };
420
+ if (!wantImages) await setCached(cacheKey, JSON.stringify(document));
421
+ return document;
422
+ }
423
+ };
424
+ /**
425
+ * Word (`.docx`) processor backed by Kreuzberg.
426
+ *
427
+ * Splits the text into heading-delimited parts (markdown). When the optional
428
+ * `@kreuzberg/node` package is missing, `process()` resolves with
429
+ * `document.unsupported` set and no parts.
430
+ */
431
+ var DOCXProcessor = class extends OfficeProcessor {
432
+ format = "docx";
433
+ label = "Word Document";
434
+ supports(type) {
435
+ const t = type.toLowerCase();
436
+ return t === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || t.endsWith(".docx") || t === "docx";
437
+ }
438
+ };
439
+ /**
440
+ * PowerPoint (`.pptx`) processor backed by Kreuzberg.
441
+ *
442
+ * One part per slide, with `notes` carrying the speaker notes and `images`
443
+ * the pictures embedded on that slide. When the optional `@kreuzberg/node`
444
+ * package is missing, `process()` resolves with `document.unsupported` set.
445
+ */
446
+ var PPTXProcessor = class extends OfficeProcessor {
447
+ format = "pptx";
448
+ label = "Presentation";
449
+ supports(type) {
450
+ const t = type.toLowerCase();
451
+ return t === "application/vnd.openxmlformats-officedocument.presentationml.presentation" || t.endsWith(".pptx") || t === "pptx";
452
+ }
453
+ };
454
+ //#endregion
180
455
  //#region src/processors/pdf.ts
181
456
  /**
182
457
  * PDF Document Processor
@@ -280,7 +555,11 @@ var PDFProcessor = class {
280
555
  /**
281
556
  * Available document processors
282
557
  */
283
- var processors = [new PDFProcessor()];
558
+ var processors = [
559
+ new PDFProcessor(),
560
+ new DOCXProcessor(),
561
+ new PPTXProcessor()
562
+ ];
284
563
  /**
285
564
  * Fetch a document from a URL with automatic format detection
286
565
  *
@@ -342,10 +621,12 @@ async function fetchDocument(url, options = {}) {
342
621
  if (!type) {
343
622
  const urlLower = url.toLowerCase();
344
623
  if (urlLower.endsWith(".pdf") || urlLower.includes(".pdf?") || urlLower.includes(".pdf#")) type = "application/pdf";
624
+ else if (/\.docx(?:[?#]|$)/.test(urlLower)) type = DOCX_MIME;
625
+ else if (/\.pptx(?:[?#]|$)/.test(urlLower)) type = PPTX_MIME;
345
626
  else type = getMimeType(url) ?? "";
346
627
  }
347
628
  const processor = processors.find((p) => p.supports(type));
348
- if (!processor) throw new Error(`No processor available for document type: ${type}. Supported types: PDF (.pdf, application/pdf)`);
629
+ if (!processor) throw new Error(`No processor available for document type: ${type}. Supported types: PDF (.pdf), Word (.docx), PowerPoint (.pptx)`);
349
630
  return processor.process(url, options);
350
631
  }
351
632
  //#endregion
@@ -353,6 +634,6 @@ async function fetchDocument(url, options = {}) {
353
634
  /** @internal */
354
635
  var PACKAGE_VERSION_INITIALIZED = true;
355
636
  //#endregion
356
- export { Document, PACKAGE_VERSION_INITIALIZED, PDFProcessor, fetchDocument, getTitleFromUrl };
637
+ export { DOCXProcessor, DOCX_MIME, Document, PACKAGE_VERSION_INITIALIZED, PDFProcessor, PPTXProcessor, PPTX_MIME, extractOffice, fetchDocument, getTitleFromUrl };
357
638
 
358
639
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/document.ts","../src/utils.ts","../src/processors/pdf.ts","../src/factory.ts","../src/index.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\nimport { URL } from 'node:url';\nimport { downloadFileWithCache, getMimeType } from '@happyvertical/files';\nimport { makeSlug } from '@happyvertical/utils';\nimport type {\n DocumentPart,\n Document as DocumentType,\n FetchDocumentOptions,\n} from './types';\n\n/**\n * Base document handler with multi-part support\n *\n * Provides functionality for downloading, caching, and structuring documents\n * into hierarchical parts. Specific format processing (PDF, HTML, Markdown)\n * is handled by specialized processors.\n */\nexport class Document {\n /**\n * Flag indicating if document is from a remote source\n */\n protected isRemote = false;\n\n /**\n * Configuration options\n */\n protected options: FetchDocumentOptions;\n\n /**\n * Local file path where document is stored\n */\n private _localPath = '';\n\n /**\n * Directory used for caching files\n */\n private _cacheDir = '';\n\n /**\n * Document URL\n */\n public url: URL;\n\n /**\n * Document MIME type\n */\n public type: string;\n\n /**\n * Document parts (hierarchical structure)\n */\n public parts: DocumentPart[] = [];\n\n /**\n * Document-level metadata\n */\n public metadata: Record<string, any> = {};\n\n /**\n * Get the local file path where document is stored\n */\n public get localPath(): string {\n return this._localPath;\n }\n\n /**\n * Get the directory used for caching files\n */\n public get cacheDir(): string {\n return this._cacheDir;\n }\n\n /**\n * Creates a new Document instance\n *\n * @param url - Document URL or file path\n * @param options - Document configuration options\n */\n constructor(url: string, options: FetchDocumentOptions = {}) {\n this.url = new URL(url);\n this.options = options;\n this.type =\n options.type || getMimeType(this.url.toString()) || 'text/plain';\n\n this._cacheDir =\n options.cacheDir ||\n path.resolve(os.tmpdir(), '.cache', 'have-sdk', 'documents');\n\n if (this.url.protocol.startsWith('file')) {\n // Decode URL-encoded characters in the pathname only (e.g., %20 -> space).\n // Note: Query parameters and hash fragments are not decoded here.\n this._localPath = decodeURIComponent(this.url.pathname);\n this.isRemote = false;\n } else if (this.url.protocol.startsWith('http')) {\n // Generate cache path from URL pathname\n // Query parameters (?) and fragments (#) are automatically excluded from url.pathname\n let pathname = this.url.pathname;\n\n // Remove trailing slash (directory-style URLs)\n if (pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n // Add file extension if missing and we know the type\n // This is crucial for URLs like /download/file/?wpdmdl=123 which have no extension\n if (!pathname.match(/\\.[a-z0-9]+$/i)) {\n // Add appropriate extension based on MIME type\n if (\n this.type === 'application/pdf' ||\n options.type === 'application/pdf'\n ) {\n pathname += '.pdf';\n }\n // Future: Add other common extensions (html, json, etc.)\n }\n\n this._localPath = path.join(\n this._cacheDir,\n makeSlug(this.url.hostname),\n pathname,\n );\n this.isRemote = true;\n }\n }\n\n /**\n * Creates and initializes a Document instance\n *\n * Downloads remote files and prepares the document for processing.\n *\n * @param url - Document URL or file path\n * @param options - Document configuration options\n * @returns Promise resolving to the initialized Document\n */\n static async create(\n url: string,\n options: FetchDocumentOptions = {},\n ): Promise<Document> {\n const document = new Document(url, options);\n await document.initialize();\n return document;\n }\n\n /**\n * Initializes the document, downloading it if it's remote\n *\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n if (this.isRemote) {\n if (!this.url) {\n throw new Error('Cannot initialize remote document: URL is required');\n }\n await downloadFileWithCache(this.url.toString(), this._localPath);\n }\n }\n\n /**\n * Checks if the document is a text-based file that can be read directly\n *\n * @returns Boolean indicating if the file is text-based\n */\n public isTextFile(): boolean {\n if (!this.type) return false;\n\n return (\n this.type.startsWith('text/') ||\n this.type === 'application/json' ||\n this.type === 'application/xml' ||\n this.type === 'application/javascript' ||\n this.type === 'application/typescript' ||\n [\n '.txt',\n '.md',\n '.json',\n '.xml',\n '.html',\n '.css',\n '.js',\n '.ts',\n '.yaml',\n '.yml',\n ].some((ext) => this.localPath.toLowerCase().endsWith(ext))\n );\n }\n\n /**\n * Converts the document to the standard Document interface\n *\n * @returns Document object with URL, type, parts, and metadata\n */\n public toDocument(): DocumentType {\n return {\n url: this.url.toString(),\n type: this.type,\n parts: this.parts,\n metadata: this.metadata,\n };\n }\n}\n\nexport default Document;\n","/**\n * Utility functions for document processing\n */\n\n/**\n * Extract a human-readable title from a URL\n *\n * Takes a URL and extracts the filename from the pathname, then formats it\n * into a readable title by removing the extension and converting separators\n * to spaces. Also decodes URL-encoded characters like %20.\n *\n * @param url - URL string to extract title from\n * @param defaultTitle - Default title to use if extraction fails\n * @returns Formatted title string\n *\n * @example\n * ```typescript\n * getTitleFromUrl('file:///path/to/My%20Document.pdf')\n * // Returns: 'My Document'\n *\n * getTitleFromUrl('https://example.com/research_paper.pdf')\n * // Returns: 'research paper'\n * ```\n */\nexport function getTitleFromUrl(\n url: string,\n defaultTitle = 'Document',\n): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const filename = pathname.split('/').pop() || defaultTitle;\n\n // Decode URL-encoded characters (e.g., %20 -> space)\n const decodedFilename = decodeURIComponent(filename);\n\n // Remove extension and convert separators to spaces\n return decodedFilename\n .replace(/\\.(pdf|html?|md|txt)$/i, '')\n .replace(/[-_]/g, ' ')\n .trim();\n } catch {\n return defaultTitle;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { promises as fs } from 'node:fs';\nimport { getCached, setCached } from '@happyvertical/files';\nimport { getPDFReader } from '@happyvertical/pdf';\nimport { Document as BaseDocument } from '../document';\nimport type {\n Document,\n DocumentImage,\n DocumentPart,\n DocumentProcessor,\n FetchDocumentOptions,\n} from '../types';\nimport { getTitleFromUrl } from '../utils';\n\n/**\n * PDF Document Processor\n *\n * Handles PDF documents with support for:\n * - Text extraction from PDF content via `@happyvertical/pdf`\n * - PDF header validation (detects HTML cache poisoning from document management systems)\n * - Processed document caching via `@happyvertical/files`\n *\n * Image extraction and OCR are stubbed for future implementation.\n */\nexport class PDFProcessor implements DocumentProcessor {\n /**\n * Check if this processor supports the given MIME type or extension.\n * Accepts `'application/pdf'`, `'.pdf'`, or `'pdf'` (case-insensitive).\n *\n * @param type - MIME type or file extension to check\n * @returns `true` if this processor can handle the given type\n */\n supports(type: string): boolean {\n return (\n type === 'application/pdf' ||\n type.endsWith('.pdf') ||\n type.toLowerCase() === 'pdf'\n );\n }\n\n /**\n * Process a PDF document\n *\n * Extracts text and optionally images/OCR from the PDF, structuring\n * it into hierarchical document parts.\n *\n * @param url - PDF URL or file path\n * @param options - Processing options\n * @returns Promise resolving to structured Document\n */\n async process(\n url: string,\n options: FetchDocumentOptions = {},\n ): Promise<Document> {\n // Create and initialize base document\n const baseDoc = await BaseDocument.create(url, options);\n\n // Check cache for processed document\n const cacheKey = `${baseDoc.localPath}.processed_pdf`;\n const cached = await getCached(cacheKey);\n if (cached) {\n try {\n const parsed = JSON.parse(cached);\n return {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: parsed.parts,\n metadata: parsed.metadata || {},\n };\n } catch (error) {\n // Cache corrupted, continue with fresh processing\n console.warn('Cached PDF data corrupted, reprocessing', error);\n }\n }\n\n // Validate that the downloaded file is actually a PDF (issue #460, #463)\n // WordPress Download Manager and some other servers may return HTML\n // with Content-Type: application/pdf, causing PDF extraction to fail\n const fileBuffer = await fs.readFile(baseDoc.localPath);\n const header = fileBuffer.subarray(0, 5).toString('utf-8');\n\n if (header !== '%PDF-') {\n // File is not a valid PDF - delete poisoned cache file (issue #463)\n try {\n await fs.unlink(baseDoc.localPath);\n } catch (unlinkError) {\n console.warn(\n `Failed to delete poisoned cache file: ${baseDoc.localPath}`,\n unlinkError,\n );\n }\n\n // Check if it's HTML to provide helpful error message\n const content = fileBuffer.toString(\n 'utf-8',\n 0,\n Math.min(1000, fileBuffer.length),\n );\n if (content.includes('<!DOCTYPE html>') || content.includes('<html')) {\n throw new Error(\n `Downloaded file is HTML, not PDF. The server returned HTML content for ${url}. ` +\n 'This commonly occurs with WordPress Download Manager URLs that return tracking pages. ' +\n `Expected PDF magic bytes (%PDF-) but got: ${header}. ` +\n 'The poisoned cache file has been removed - please try again.',\n );\n } else {\n throw new Error(\n `Downloaded file is not a valid PDF. Expected %PDF- magic bytes but got: ${header}. ` +\n 'The invalid cache file has been removed - please try again.',\n );\n }\n }\n\n // Get PDF reader and extract content\n const reader = await getPDFReader();\n const extractedText = await reader.extractText(baseDoc.localPath);\n\n // Create main document part\n const mainPart: DocumentPart = {\n id: randomUUID(),\n title: getTitleFromUrl(url, 'PDF Document'),\n content: extractedText || '',\n type: 'text',\n metadata: {\n source: 'pdf',\n filePath: baseDoc.localPath,\n },\n };\n\n // Extract images if enabled\n if (options.extractImages === true) {\n mainPart.images = await this.extractImages(\n baseDoc.localPath,\n options.runOcr !== false,\n );\n }\n\n const document: Document = {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: [mainPart],\n metadata: {\n processor: 'pdf',\n extractedAt: new Date().toISOString(),\n hasImages: (mainPart.images?.length || 0) > 0,\n },\n };\n\n // Cache the processed document\n await setCached(cacheKey, JSON.stringify(document));\n\n return document;\n }\n\n /**\n * Extract images from PDF\n *\n * This is a placeholder for future image extraction functionality.\n * Will use @happyvertical/pdf's image extraction capabilities when available.\n *\n * @param filePath - Local PDF file path\n * @param runOcr - Whether to run OCR on extracted images\n * @returns Promise resolving to array of DocumentImages\n */\n private async extractImages(\n _filePath: string,\n _runOcr: boolean,\n ): Promise<DocumentImage[]> {\n // TODO: Implement image extraction using @happyvertical/pdf\n // For now, return empty array as placeholder\n\n // Future implementation will:\n // 1. Use getPDFReader() to extract images from PDF\n // 2. Save images to cache directory\n // 3. If runOcr is true, use @happyvertical/ocr to extract text from images\n // 4. Return array of DocumentImage objects with metadata\n\n return [];\n }\n}\n\nexport default PDFProcessor;\n","import { getMimeType } from '@happyvertical/files';\nimport { scrapeDocument } from '@happyvertical/spider';\nimport { PDFProcessor } from './processors/pdf';\nimport type { Document, FetchDocumentOptions } from './types';\n\n/**\n * Available document processors\n */\nconst processors = [new PDFProcessor()];\n\n/**\n * Fetch a document from a URL with automatic format detection\n *\n * This factory function:\n * 1. Detects the document format (PDF, HTML, Markdown, etc.)\n * 2. Selects the appropriate processor\n * 3. Processes the document into structured parts\n * 4. Returns a Document object with hierarchical content\n *\n * @param url - Document URL or file path (file://, http://, https://)\n * @param options - Fetch and processing options\n * @returns Promise resolving to structured Document\n *\n * @example\n * ```typescript\n * // Fetch a PDF with image extraction and OCR\n * const doc = await fetchDocument('https://example.com/report.pdf', {\n * extractImages: true,\n * runOcr: true\n * });\n *\n * // Access document parts\n * for (const part of doc.parts) {\n * console.log(part.title);\n * console.log(part.content);\n *\n * // Check for images\n * if (part.images) {\n * for (const image of part.images) {\n * console.log(image.url);\n * console.log(image.ocrText); // Text extracted via OCR\n * }\n * }\n * }\n * ```\n */\nexport async function fetchDocument(\n url: string,\n options: FetchDocumentOptions = {},\n): Promise<Document> {\n // For web URLs (http/https), use spider package to detect special cases\n // (WordPress Download Manager, CivicWeb, DocuShare, etc.)\n const isWebUrl = url.startsWith('http://') || url.startsWith('https://');\n\n if (isWebUrl && !options.type) {\n try {\n const scraper =\n options.scraper === 'crawlee' ? 'basic' : (options.scraper ?? 'basic');\n const spider =\n options.spider ??\n options.spiderAdapter ??\n (options.scraper === 'crawlee' ? 'crawlee' : 'dom');\n\n // Use spider to detect WordPress, CivicWeb, DocuShare, and other document management systems\n const scraped = await scrapeDocument(url, {\n scraper,\n spider,\n cache: options.cache,\n cacheExpiry: options.cacheExpiry,\n headers: options.headers,\n timeout: options.timeout,\n maxDuration: options.maxDuration,\n maxInteractions: options.maxInteractions,\n });\n\n // Check if spider detected a document management system with PDF link\n const hasDocLink =\n scraped.metadata.strategy === 'wordpress-pdf-link' ||\n scraped.metadata.strategy === 'civicweb-pdf-link' ||\n scraped.metadata.strategy === 'docushare-pdf-link';\n\n if (hasDocLink && scraped.metadata.isPdf && !scraped.metadata.complete) {\n // Spider detected a document management page and extracted the PDF URL\n // Use the extracted URL for PDF processing\n url = scraped.url;\n options.type = 'application/pdf';\n }\n } catch (error) {\n // If spider fails, continue with direct download\n // This ensures backward compatibility\n console.warn(\n `Spider detection failed for ${url}, falling back to direct download:`,\n error,\n );\n }\n }\n\n // Determine type - check URL extension first, then MIME type\n // This handles servers that return incorrect Content-Type headers (e.g., application/octet-stream for PDFs)\n let type = options.type ?? '';\n\n if (!type) {\n // Extract file extension from URL\n const urlLower = url.toLowerCase();\n\n // Check for common document extensions in URL\n if (\n urlLower.endsWith('.pdf') ||\n urlLower.includes('.pdf?') ||\n urlLower.includes('.pdf#')\n ) {\n type = 'application/pdf';\n } else {\n // Fall back to MIME type detection\n type = getMimeType(url) ?? '';\n }\n }\n\n // Find appropriate processor\n const processor = processors.find((p) => p.supports(type));\n\n if (!processor) {\n throw new Error(\n `No processor available for document type: ${type}. Supported types: PDF (.pdf, application/pdf)`,\n );\n }\n\n // Process document\n return processor.process(url, options);\n}\n\nexport default fetchDocument;\n","/**\n * @happyvertical/documents - Document processing with multi-part structure\n *\n * Provides document processing for PDFs with support for:\n * - Hierarchical document parts\n * - Automatic format detection from URL or MIME type\n * - Document management system detection (WordPress, CivicWeb, DocuShare)\n * - File caching for performance\n *\n * @example\n * ```typescript\n * import { fetchDocument } from '@happyvertical/documents';\n *\n * const doc = await fetchDocument('https://example.com/report.pdf');\n *\n * for (const part of doc.parts) {\n * console.log(part.title);\n * console.log(part.content);\n * }\n * ```\n */\n\n// Base classes\nexport { Document } from './document';\n// Main factory function\nexport { fetchDocument } from './factory';\n\n// Processors\nexport { PDFProcessor } from './processors/pdf';\n// Types\nexport type {\n Document as DocumentType,\n DocumentImage,\n DocumentPart,\n DocumentProcessor,\n FetchDocumentOptions,\n} from './types';\n// Utilities\nexport { getTitleFromUrl } from './utils';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/document.ts","../src/utils.ts","../src/processors/office.ts","../src/processors/pdf.ts","../src/factory.ts","../src/index.ts"],"sourcesContent":["import os from 'node:os';\nimport path from 'node:path';\nimport { URL } from 'node:url';\nimport { downloadFileWithCache, getMimeType } from '@happyvertical/files';\nimport { makeSlug } from '@happyvertical/utils';\nimport type {\n DocumentPart,\n Document as DocumentType,\n FetchDocumentOptions,\n} from './types';\n\n/**\n * Base document handler with multi-part support\n *\n * Provides functionality for downloading, caching, and structuring documents\n * into hierarchical parts. Specific format processing (PDF, HTML, Markdown)\n * is handled by specialized processors.\n */\nexport class Document {\n /**\n * Flag indicating if document is from a remote source\n */\n protected isRemote = false;\n\n /**\n * Configuration options\n */\n protected options: FetchDocumentOptions;\n\n /**\n * Local file path where document is stored\n */\n private _localPath = '';\n\n /**\n * Directory used for caching files\n */\n private _cacheDir = '';\n\n /**\n * Document URL\n */\n public url: URL;\n\n /**\n * Document MIME type\n */\n public type: string;\n\n /**\n * Document parts (hierarchical structure)\n */\n public parts: DocumentPart[] = [];\n\n /**\n * Document-level metadata\n */\n public metadata: Record<string, any> = {};\n\n /**\n * Get the local file path where document is stored\n */\n public get localPath(): string {\n return this._localPath;\n }\n\n /**\n * Get the directory used for caching files\n */\n public get cacheDir(): string {\n return this._cacheDir;\n }\n\n /**\n * Creates a new Document instance\n *\n * @param url - Document URL or file path\n * @param options - Document configuration options\n */\n constructor(url: string, options: FetchDocumentOptions = {}) {\n this.url = new URL(url);\n this.options = options;\n this.type =\n options.type || getMimeType(this.url.toString()) || 'text/plain';\n\n this._cacheDir =\n options.cacheDir ||\n path.resolve(os.tmpdir(), '.cache', 'have-sdk', 'documents');\n\n if (this.url.protocol.startsWith('file')) {\n // Decode URL-encoded characters in the pathname only (e.g., %20 -> space).\n // Note: Query parameters and hash fragments are not decoded here.\n this._localPath = decodeURIComponent(this.url.pathname);\n this.isRemote = false;\n } else if (this.url.protocol.startsWith('http')) {\n // Generate cache path from URL pathname\n // Query parameters (?) and fragments (#) are automatically excluded from url.pathname\n let pathname = this.url.pathname;\n\n // Remove trailing slash (directory-style URLs)\n if (pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n // Add file extension if missing and we know the type\n // This is crucial for URLs like /download/file/?wpdmdl=123 which have no extension\n if (!pathname.match(/\\.[a-z0-9]+$/i)) {\n // Add appropriate extension based on MIME type\n if (\n this.type === 'application/pdf' ||\n options.type === 'application/pdf'\n ) {\n pathname += '.pdf';\n }\n // Future: Add other common extensions (html, json, etc.)\n }\n\n this._localPath = path.join(\n this._cacheDir,\n makeSlug(this.url.hostname),\n pathname,\n );\n this.isRemote = true;\n }\n }\n\n /**\n * Creates and initializes a Document instance\n *\n * Downloads remote files and prepares the document for processing.\n *\n * @param url - Document URL or file path\n * @param options - Document configuration options\n * @returns Promise resolving to the initialized Document\n */\n static async create(\n url: string,\n options: FetchDocumentOptions = {},\n ): Promise<Document> {\n const document = new Document(url, options);\n await document.initialize();\n return document;\n }\n\n /**\n * Initializes the document, downloading it if it's remote\n *\n * @returns Promise that resolves when initialization is complete\n */\n async initialize(): Promise<void> {\n if (this.isRemote) {\n if (!this.url) {\n throw new Error('Cannot initialize remote document: URL is required');\n }\n await downloadFileWithCache(this.url.toString(), this._localPath);\n }\n }\n\n /**\n * Checks if the document is a text-based file that can be read directly\n *\n * @returns Boolean indicating if the file is text-based\n */\n public isTextFile(): boolean {\n if (!this.type) return false;\n\n return (\n this.type.startsWith('text/') ||\n this.type === 'application/json' ||\n this.type === 'application/xml' ||\n this.type === 'application/javascript' ||\n this.type === 'application/typescript' ||\n [\n '.txt',\n '.md',\n '.json',\n '.xml',\n '.html',\n '.css',\n '.js',\n '.ts',\n '.yaml',\n '.yml',\n ].some((ext) => this.localPath.toLowerCase().endsWith(ext))\n );\n }\n\n /**\n * Converts the document to the standard Document interface\n *\n * @returns Document object with URL, type, parts, and metadata\n */\n public toDocument(): DocumentType {\n return {\n url: this.url.toString(),\n type: this.type,\n parts: this.parts,\n metadata: this.metadata,\n };\n }\n}\n\nexport default Document;\n","/**\n * Utility functions for document processing\n */\n\n/**\n * Extract a human-readable title from a URL\n *\n * Takes a URL and extracts the filename from the pathname, then formats it\n * into a readable title by removing the extension and converting separators\n * to spaces. Also decodes URL-encoded characters like %20.\n *\n * @param url - URL string to extract title from\n * @param defaultTitle - Default title to use if extraction fails\n * @returns Formatted title string\n *\n * @example\n * ```typescript\n * getTitleFromUrl('file:///path/to/My%20Document.pdf')\n * // Returns: 'My Document'\n *\n * getTitleFromUrl('https://example.com/research_paper.pdf')\n * // Returns: 'research paper'\n * ```\n */\nexport function getTitleFromUrl(\n url: string,\n defaultTitle = 'Document',\n): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const filename = pathname.split('/').pop() || defaultTitle;\n\n // Decode URL-encoded characters (e.g., %20 -> space)\n const decodedFilename = decodeURIComponent(filename);\n\n // Remove extension and convert separators to spaces\n return decodedFilename\n .replace(/\\.(pdf|html?|md|txt)$/i, '')\n .replace(/[-_]/g, ' ')\n .trim();\n } catch {\n return defaultTitle;\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { promises as fs } from 'node:fs';\nimport { getCached, setCached } from '@happyvertical/files';\nimport { Document as BaseDocument } from '../document';\nimport type {\n Document,\n DocumentImage,\n DocumentPart,\n DocumentProcessor,\n FetchDocumentOptions,\n UnsupportedReason,\n} from '../types';\nimport { getTitleFromUrl } from '../utils';\n\n/** Office formats read through Kreuzberg. */\nexport type OfficeFormat = 'docx' | 'pptx';\n\nexport const DOCX_MIME =\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';\nexport const PPTX_MIME =\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation';\n\nconst MIME: Record<OfficeFormat, string> = {\n docx: DOCX_MIME,\n pptx: PPTX_MIME,\n};\n\nconst KREUZBERG_PACKAGE = '@kreuzberg/node';\n\n/** Options for {@link extractOffice}. */\nexport interface ExtractOfficeOptions {\n /** Return embedded images (bytes in memory). @default true */\n extractImages?: boolean;\n /** Fallback title when the file carries none. */\n title?: string;\n}\n\n/** A successful extraction. */\nexport interface OfficeExtraction {\n status: 'ok';\n format: OfficeFormat;\n /** Document title from the file's properties, when present. */\n title?: string;\n /**\n * DOCX: one part per top-level heading section (nested by heading level),\n * `type: 'markdown'`. PPTX: one part per slide in order, `type: 'text'`,\n * with `notes` holding that slide's speaker notes.\n */\n parts: DocumentPart[];\n metadata: Record<string, unknown>;\n}\n\n/** The Kreuzberg engine is not installed; nothing was read. */\nexport interface OfficeUnsupported extends UnsupportedReason {\n status: 'unsupported';\n format: OfficeFormat;\n}\n\nexport type OfficeResult = OfficeExtraction | OfficeUnsupported;\n\n// Structural subset of @kreuzberg/node so the optional module is only a\n// runtime concern.\ninterface KreuzbergImage {\n data: Uint8Array;\n format: string;\n imageIndex: number;\n pageNumber?: number | null;\n width?: number | null;\n height?: number | null;\n description?: string | null;\n}\ninterface KreuzbergResult {\n content?: string | null;\n metadata?: Record<string, unknown> & { title?: string | null };\n images?: KreuzbergImage[] | null;\n}\ninterface KreuzbergModule {\n extractBytes(\n data: Buffer,\n mimeType: string,\n config?: Record<string, unknown> | null,\n ): Promise<KreuzbergResult>;\n}\n\nlet kreuzbergPromise: Promise<KreuzbergModule | null> | undefined;\n\n/** Lazily load the optional engine; resolves `null` when it is absent. */\nfunction loadKreuzberg(): Promise<KreuzbergModule | null> {\n kreuzbergPromise ??= import('@kreuzberg/node').then(\n (mod) => mod as unknown as KreuzbergModule,\n () => null,\n );\n return kreuzbergPromise;\n}\n\n/** @internal test hook: forget the cached engine lookup. */\nexport function resetOfficeEngineForTests(): void {\n kreuzbergPromise = undefined;\n}\n\nconst SLIDE_MARKER = (n: string) => `\\n<<hv-slide:${n}>>\\n`;\nconst SLIDE_MARKER_FORMAT = SLIDE_MARKER('{page_num}');\nconst SLIDE_MARKER_RE = /<<hv-slide:(\\d+)>>/g;\n// Kreuzberg appends a slide's speaker notes after a \"Notes:\" paragraph.\nconst NOTES_RE = /(?:^|\\n\\n)Notes:\\n\\n([\\s\\S]*)$/;\n\nfunction toImage(image: KreuzbergImage, format: OfficeFormat): DocumentImage {\n return {\n id: randomUUID(),\n url: `office-image:${format}/${image.imageIndex}`,\n data: image.data,\n altText: image.description ?? undefined,\n position: image.imageIndex,\n metadata: {\n format: image.format,\n width: image.width ?? undefined,\n height: image.height ?? undefined,\n ...(image.pageNumber ? { slide: image.pageNumber } : {}),\n },\n };\n}\n\nfunction splitSlides(content: string): Map<number, string> {\n const slides = new Map<number, string>();\n const matches = [...content.matchAll(SLIDE_MARKER_RE)];\n matches.forEach((match, i) => {\n const start = (match.index ?? 0) + match[0].length;\n const end = matches[i + 1]?.index ?? content.length;\n slides.set(Number(match[1]), content.slice(start, end).trim());\n });\n return slides;\n}\n\nfunction buildSlideParts(\n content: string,\n images: KreuzbergImage[],\n): DocumentPart[] {\n const slides = splitSlides(content);\n if (slides.size === 0) {\n // Engine did not honour page markers: still return the text, unsplit.\n const text = content.trim();\n return text\n ? [\n {\n id: randomUUID(),\n title: 'Presentation',\n content: text,\n type: 'text',\n metadata: { source: 'pptx', slidesSplit: false },\n },\n ]\n : [];\n }\n return [...slides.entries()].map(([slide, raw]) => {\n const notesMatch = NOTES_RE.exec(raw);\n const text = (notesMatch ? raw.slice(0, notesMatch.index) : raw).trim();\n const notes = notesMatch?.[1]?.trim();\n const slideImages = images\n .filter((img) => img.pageNumber === slide)\n .map((img) => toImage(img, 'pptx'));\n const part: DocumentPart = {\n id: randomUUID(),\n title:\n text\n .split('\\n')\n .find((line) => line.trim())\n ?.trim() || `Slide ${slide}`,\n content: text,\n type: 'text',\n metadata: { source: 'pptx', slide },\n };\n if (notes) part.notes = notes;\n if (slideImages.length > 0) part.images = slideImages;\n return part;\n });\n}\n\ninterface Section {\n level: number;\n title: string;\n lines: string[];\n children: Section[];\n}\n\nfunction buildDocxParts(\n markdown: string,\n fallbackTitle: string,\n images: DocumentImage[],\n): DocumentPart[] {\n const root: Section = { level: 0, title: '', lines: [], children: [] };\n const stack: Section[] = [root];\n let inFence = false;\n for (const line of markdown.split('\\n')) {\n if (/^\\s*```/.test(line)) inFence = !inFence;\n const heading = inFence ? null : /^(#{1,6})\\s+(.*\\S)\\s*$/.exec(line);\n if (!heading) {\n stack[stack.length - 1].lines.push(line);\n continue;\n }\n const section: Section = {\n level: heading[1].length,\n title: heading[2],\n lines: [],\n children: [],\n };\n while (stack[stack.length - 1].level >= section.level) stack.pop();\n stack[stack.length - 1].children.push(section);\n stack.push(section);\n }\n\n const toPart = (section: Section): DocumentPart => {\n const part: DocumentPart = {\n id: randomUUID(),\n title: section.title,\n content: section.lines.join('\\n').trim(),\n type: 'markdown',\n metadata: { source: 'docx', headingLevel: section.level },\n };\n if (section.children.length > 0) part.parts = section.children.map(toPart);\n return part;\n };\n\n const parts: DocumentPart[] = [];\n const lead = root.lines.join('\\n').trim();\n if (lead || root.children.length === 0) {\n if (lead) {\n parts.push({\n id: randomUUID(),\n title: fallbackTitle,\n content: lead,\n type: 'markdown',\n metadata: { source: 'docx', headingLevel: 0 },\n });\n }\n }\n parts.push(...root.children.map(toPart));\n if (images.length > 0) {\n // An image-only document still needs a part to carry its pictures.\n if (parts.length === 0) {\n parts.push({\n id: randomUUID(),\n title: fallbackTitle,\n content: '',\n type: 'markdown',\n metadata: { source: 'docx', headingLevel: 0 },\n });\n }\n parts[0].images = images;\n }\n return parts;\n}\n\n/**\n * Read a DOCX or PPTX from memory (or a path) through Kreuzberg.\n *\n * Returns `{ status: 'unsupported' }` -- never throws for that case -- when the\n * optional `@kreuzberg/node` dependency is not installed. Corrupt or\n * unreadable files reject with the engine's error.\n *\n * @param source - File bytes or a local file path\n * @param format - `'docx'` or `'pptx'`\n */\nexport async function extractOffice(\n source: Buffer | Uint8Array | string,\n format: OfficeFormat,\n options: ExtractOfficeOptions = {},\n): Promise<OfficeResult> {\n const kreuzberg = await loadKreuzberg();\n if (!kreuzberg) {\n return {\n status: 'unsupported',\n code: 'dependency-missing',\n format,\n dependency: KREUZBERG_PACKAGE,\n reason: `Reading .${format} files requires the optional ${KREUZBERG_PACKAGE} package, which is not installed.`,\n };\n }\n\n const bytes =\n typeof source === 'string'\n ? await fs.readFile(source)\n : Buffer.isBuffer(source)\n ? source\n : Buffer.from(source);\n const wantImages = options.extractImages !== false;\n const config: Record<string, unknown> = {\n images: { extractImages: wantImages },\n };\n if (format === 'pptx') {\n config.pages = {\n extractPages: true,\n insertPageMarkers: true,\n markerFormat: SLIDE_MARKER_FORMAT,\n };\n } else {\n config.outputFormat = 'markdown';\n }\n\n const result = await kreuzberg.extractBytes(bytes, MIME[format], config);\n const content = result.content ?? '';\n const rawImages = wantImages ? (result.images ?? []) : [];\n const title = result.metadata?.title?.trim() || undefined;\n\n const parts =\n format === 'pptx'\n ? buildSlideParts(content, rawImages)\n : buildDocxParts(\n content,\n title ?? options.title ?? 'Document',\n rawImages.map((img) => toImage(img, 'docx')),\n );\n\n return {\n status: 'ok',\n format,\n title,\n parts,\n metadata: {\n processor: format,\n engine: 'kreuzberg',\n extractedAt: new Date().toISOString(),\n hasImages: rawImages.length > 0,\n ...(format === 'pptx'\n ? {\n slideCount: parts.filter((p) => p.metadata?.slide !== undefined)\n .length,\n }\n : {}),\n ...(title ? { title } : {}),\n },\n };\n}\n\nabstract class OfficeProcessor implements DocumentProcessor {\n protected abstract readonly format: OfficeFormat;\n protected abstract readonly label: string;\n\n abstract supports(type: string): boolean;\n\n async process(\n url: string,\n options: FetchDocumentOptions = {},\n ): Promise<Document> {\n const baseDoc = await BaseDocument.create(url, {\n ...options,\n type: options.type ?? MIME[this.format],\n });\n\n const cacheKey = `${baseDoc.localPath}.processed_${this.format}`;\n const wantImages = options.extractImages !== false;\n // Images are held in memory, so only text results are cached.\n const cached = wantImages ? null : await getCached(cacheKey);\n if (cached) {\n try {\n const parsed = JSON.parse(cached);\n return {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: parsed.parts,\n metadata: parsed.metadata || {},\n };\n } catch (error) {\n console.warn(\n `Cached ${this.label} data corrupted, reprocessing`,\n error,\n );\n }\n }\n\n const result = await extractOffice(baseDoc.localPath, this.format, {\n extractImages: wantImages,\n title: getTitleFromUrl(url, this.label),\n });\n\n if (result.status === 'unsupported') {\n return {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: [],\n metadata: { processor: this.format, unsupported: true },\n unsupported: {\n code: result.code,\n reason: result.reason,\n dependency: result.dependency,\n },\n };\n }\n\n const document: Document = {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: result.parts,\n metadata: result.metadata,\n };\n if (!wantImages) await setCached(cacheKey, JSON.stringify(document));\n return document;\n }\n}\n\n/**\n * Word (`.docx`) processor backed by Kreuzberg.\n *\n * Splits the text into heading-delimited parts (markdown). When the optional\n * `@kreuzberg/node` package is missing, `process()` resolves with\n * `document.unsupported` set and no parts.\n */\nexport class DOCXProcessor extends OfficeProcessor {\n protected readonly format = 'docx' as const;\n protected readonly label = 'Word Document';\n\n supports(type: string): boolean {\n const t = type.toLowerCase();\n return t === DOCX_MIME || t.endsWith('.docx') || t === 'docx';\n }\n}\n\n/**\n * PowerPoint (`.pptx`) processor backed by Kreuzberg.\n *\n * One part per slide, with `notes` carrying the speaker notes and `images`\n * the pictures embedded on that slide. When the optional `@kreuzberg/node`\n * package is missing, `process()` resolves with `document.unsupported` set.\n */\nexport class PPTXProcessor extends OfficeProcessor {\n protected readonly format = 'pptx' as const;\n protected readonly label = 'Presentation';\n\n supports(type: string): boolean {\n const t = type.toLowerCase();\n return t === PPTX_MIME || t.endsWith('.pptx') || t === 'pptx';\n }\n}\n","import { randomUUID } from 'node:crypto';\nimport { promises as fs } from 'node:fs';\nimport { getCached, setCached } from '@happyvertical/files';\nimport { getPDFReader } from '@happyvertical/pdf';\nimport { Document as BaseDocument } from '../document';\nimport type {\n Document,\n DocumentImage,\n DocumentPart,\n DocumentProcessor,\n FetchDocumentOptions,\n} from '../types';\nimport { getTitleFromUrl } from '../utils';\n\n/**\n * PDF Document Processor\n *\n * Handles PDF documents with support for:\n * - Text extraction from PDF content via `@happyvertical/pdf`\n * - PDF header validation (detects HTML cache poisoning from document management systems)\n * - Processed document caching via `@happyvertical/files`\n *\n * Image extraction and OCR are stubbed for future implementation.\n */\nexport class PDFProcessor implements DocumentProcessor {\n /**\n * Check if this processor supports the given MIME type or extension.\n * Accepts `'application/pdf'`, `'.pdf'`, or `'pdf'` (case-insensitive).\n *\n * @param type - MIME type or file extension to check\n * @returns `true` if this processor can handle the given type\n */\n supports(type: string): boolean {\n return (\n type === 'application/pdf' ||\n type.endsWith('.pdf') ||\n type.toLowerCase() === 'pdf'\n );\n }\n\n /**\n * Process a PDF document\n *\n * Extracts text and optionally images/OCR from the PDF, structuring\n * it into hierarchical document parts.\n *\n * @param url - PDF URL or file path\n * @param options - Processing options\n * @returns Promise resolving to structured Document\n */\n async process(\n url: string,\n options: FetchDocumentOptions = {},\n ): Promise<Document> {\n // Create and initialize base document\n const baseDoc = await BaseDocument.create(url, options);\n\n // Check cache for processed document\n const cacheKey = `${baseDoc.localPath}.processed_pdf`;\n const cached = await getCached(cacheKey);\n if (cached) {\n try {\n const parsed = JSON.parse(cached);\n return {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: parsed.parts,\n metadata: parsed.metadata || {},\n };\n } catch (error) {\n // Cache corrupted, continue with fresh processing\n console.warn('Cached PDF data corrupted, reprocessing', error);\n }\n }\n\n // Validate that the downloaded file is actually a PDF (issue #460, #463)\n // WordPress Download Manager and some other servers may return HTML\n // with Content-Type: application/pdf, causing PDF extraction to fail\n const fileBuffer = await fs.readFile(baseDoc.localPath);\n const header = fileBuffer.subarray(0, 5).toString('utf-8');\n\n if (header !== '%PDF-') {\n // File is not a valid PDF - delete poisoned cache file (issue #463)\n try {\n await fs.unlink(baseDoc.localPath);\n } catch (unlinkError) {\n console.warn(\n `Failed to delete poisoned cache file: ${baseDoc.localPath}`,\n unlinkError,\n );\n }\n\n // Check if it's HTML to provide helpful error message\n const content = fileBuffer.toString(\n 'utf-8',\n 0,\n Math.min(1000, fileBuffer.length),\n );\n if (content.includes('<!DOCTYPE html>') || content.includes('<html')) {\n throw new Error(\n `Downloaded file is HTML, not PDF. The server returned HTML content for ${url}. ` +\n 'This commonly occurs with WordPress Download Manager URLs that return tracking pages. ' +\n `Expected PDF magic bytes (%PDF-) but got: ${header}. ` +\n 'The poisoned cache file has been removed - please try again.',\n );\n } else {\n throw new Error(\n `Downloaded file is not a valid PDF. Expected %PDF- magic bytes but got: ${header}. ` +\n 'The invalid cache file has been removed - please try again.',\n );\n }\n }\n\n // Get PDF reader and extract content\n const reader = await getPDFReader();\n const extractedText = await reader.extractText(baseDoc.localPath);\n\n // Create main document part\n const mainPart: DocumentPart = {\n id: randomUUID(),\n title: getTitleFromUrl(url, 'PDF Document'),\n content: extractedText || '',\n type: 'text',\n metadata: {\n source: 'pdf',\n filePath: baseDoc.localPath,\n },\n };\n\n // Extract images if enabled\n if (options.extractImages === true) {\n mainPart.images = await this.extractImages(\n baseDoc.localPath,\n options.runOcr !== false,\n );\n }\n\n const document: Document = {\n url: baseDoc.url.toString(),\n type: baseDoc.type,\n parts: [mainPart],\n metadata: {\n processor: 'pdf',\n extractedAt: new Date().toISOString(),\n hasImages: (mainPart.images?.length || 0) > 0,\n },\n };\n\n // Cache the processed document\n await setCached(cacheKey, JSON.stringify(document));\n\n return document;\n }\n\n /**\n * Extract images from PDF\n *\n * This is a placeholder for future image extraction functionality.\n * Will use @happyvertical/pdf's image extraction capabilities when available.\n *\n * @param filePath - Local PDF file path\n * @param runOcr - Whether to run OCR on extracted images\n * @returns Promise resolving to array of DocumentImages\n */\n private async extractImages(\n _filePath: string,\n _runOcr: boolean,\n ): Promise<DocumentImage[]> {\n // TODO: Implement image extraction using @happyvertical/pdf\n // For now, return empty array as placeholder\n\n // Future implementation will:\n // 1. Use getPDFReader() to extract images from PDF\n // 2. Save images to cache directory\n // 3. If runOcr is true, use @happyvertical/ocr to extract text from images\n // 4. Return array of DocumentImage objects with metadata\n\n return [];\n }\n}\n\nexport default PDFProcessor;\n","import { getMimeType } from '@happyvertical/files';\nimport { scrapeDocument } from '@happyvertical/spider';\nimport {\n DOCX_MIME,\n DOCXProcessor,\n PPTX_MIME,\n PPTXProcessor,\n} from './processors/office';\nimport { PDFProcessor } from './processors/pdf';\nimport type { Document, FetchDocumentOptions } from './types';\n\n/**\n * Available document processors\n */\nconst processors = [\n new PDFProcessor(),\n new DOCXProcessor(),\n new PPTXProcessor(),\n];\n\n/**\n * Fetch a document from a URL with automatic format detection\n *\n * This factory function:\n * 1. Detects the document format (PDF, HTML, Markdown, etc.)\n * 2. Selects the appropriate processor\n * 3. Processes the document into structured parts\n * 4. Returns a Document object with hierarchical content\n *\n * @param url - Document URL or file path (file://, http://, https://)\n * @param options - Fetch and processing options\n * @returns Promise resolving to structured Document\n *\n * @example\n * ```typescript\n * // Fetch a PDF with image extraction and OCR\n * const doc = await fetchDocument('https://example.com/report.pdf', {\n * extractImages: true,\n * runOcr: true\n * });\n *\n * // Access document parts\n * for (const part of doc.parts) {\n * console.log(part.title);\n * console.log(part.content);\n *\n * // Check for images\n * if (part.images) {\n * for (const image of part.images) {\n * console.log(image.url);\n * console.log(image.ocrText); // Text extracted via OCR\n * }\n * }\n * }\n * ```\n */\nexport async function fetchDocument(\n url: string,\n options: FetchDocumentOptions = {},\n): Promise<Document> {\n // For web URLs (http/https), use spider package to detect special cases\n // (WordPress Download Manager, CivicWeb, DocuShare, etc.)\n const isWebUrl = url.startsWith('http://') || url.startsWith('https://');\n\n if (isWebUrl && !options.type) {\n try {\n const scraper =\n options.scraper === 'crawlee' ? 'basic' : (options.scraper ?? 'basic');\n const spider =\n options.spider ??\n options.spiderAdapter ??\n (options.scraper === 'crawlee' ? 'crawlee' : 'dom');\n\n // Use spider to detect WordPress, CivicWeb, DocuShare, and other document management systems\n const scraped = await scrapeDocument(url, {\n scraper,\n spider,\n cache: options.cache,\n cacheExpiry: options.cacheExpiry,\n headers: options.headers,\n timeout: options.timeout,\n maxDuration: options.maxDuration,\n maxInteractions: options.maxInteractions,\n });\n\n // Check if spider detected a document management system with PDF link\n const hasDocLink =\n scraped.metadata.strategy === 'wordpress-pdf-link' ||\n scraped.metadata.strategy === 'civicweb-pdf-link' ||\n scraped.metadata.strategy === 'docushare-pdf-link';\n\n if (hasDocLink && scraped.metadata.isPdf && !scraped.metadata.complete) {\n // Spider detected a document management page and extracted the PDF URL\n // Use the extracted URL for PDF processing\n url = scraped.url;\n options.type = 'application/pdf';\n }\n } catch (error) {\n // If spider fails, continue with direct download\n // This ensures backward compatibility\n console.warn(\n `Spider detection failed for ${url}, falling back to direct download:`,\n error,\n );\n }\n }\n\n // Determine type - check URL extension first, then MIME type\n // This handles servers that return incorrect Content-Type headers (e.g., application/octet-stream for PDFs)\n let type = options.type ?? '';\n\n if (!type) {\n // Extract file extension from URL\n const urlLower = url.toLowerCase();\n\n // Check for common document extensions in URL\n if (\n urlLower.endsWith('.pdf') ||\n urlLower.includes('.pdf?') ||\n urlLower.includes('.pdf#')\n ) {\n type = 'application/pdf';\n } else if (/\\.docx(?:[?#]|$)/.test(urlLower)) {\n type = DOCX_MIME;\n } else if (/\\.pptx(?:[?#]|$)/.test(urlLower)) {\n type = PPTX_MIME;\n } else {\n // Fall back to MIME type detection\n type = getMimeType(url) ?? '';\n }\n }\n\n // Find appropriate processor\n const processor = processors.find((p) => p.supports(type));\n\n if (!processor) {\n throw new Error(\n `No processor available for document type: ${type}. Supported types: PDF (.pdf), Word (.docx), PowerPoint (.pptx)`,\n );\n }\n\n // Process document\n return processor.process(url, options);\n}\n\nexport default fetchDocument;\n","/**\n * @happyvertical/documents - Document processing with multi-part structure\n *\n * Provides document processing for PDF, Word (.docx) and PowerPoint (.pptx)\n * (the latter two through the optional Kreuzberg engine) with support for:\n * - Hierarchical document parts\n * - Automatic format detection from URL or MIME type\n * - Document management system detection (WordPress, CivicWeb, DocuShare)\n * - File caching for performance\n *\n * @example\n * ```typescript\n * import { fetchDocument } from '@happyvertical/documents';\n *\n * const doc = await fetchDocument('https://example.com/report.pdf');\n *\n * for (const part of doc.parts) {\n * console.log(part.title);\n * console.log(part.content);\n * }\n * ```\n */\n\n// Base classes\nexport { Document } from './document';\n// Main factory function\nexport { fetchDocument } from './factory';\n\n// Processors\nexport type {\n ExtractOfficeOptions,\n OfficeExtraction,\n OfficeFormat,\n OfficeResult,\n OfficeUnsupported,\n} from './processors/office';\nexport {\n DOCX_MIME,\n DOCXProcessor,\n extractOffice,\n PPTX_MIME,\n PPTXProcessor,\n} from './processors/office';\nexport { PDFProcessor } from './processors/pdf';\n// Types\nexport type {\n Document as DocumentType,\n DocumentImage,\n DocumentPart,\n DocumentProcessor,\n FetchDocumentOptions,\n UnsupportedReason,\n} from './types';\n// Utilities\nexport { getTitleFromUrl } from './utils';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
@@ -0,0 +1,77 @@
1
+ import type { Document, DocumentPart, DocumentProcessor, FetchDocumentOptions, UnsupportedReason } from "../types.js";
2
+ /** Office formats read through Kreuzberg. */
3
+ export type OfficeFormat = 'docx' | 'pptx';
4
+ export declare const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
5
+ export declare const PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
6
+ /** Options for {@link extractOffice}. */
7
+ export interface ExtractOfficeOptions {
8
+ /** Return embedded images (bytes in memory). @default true */
9
+ extractImages?: boolean;
10
+ /** Fallback title when the file carries none. */
11
+ title?: string;
12
+ }
13
+ /** A successful extraction. */
14
+ export interface OfficeExtraction {
15
+ status: 'ok';
16
+ format: OfficeFormat;
17
+ /** Document title from the file's properties, when present. */
18
+ title?: string;
19
+ /**
20
+ * DOCX: one part per top-level heading section (nested by heading level),
21
+ * `type: 'markdown'`. PPTX: one part per slide in order, `type: 'text'`,
22
+ * with `notes` holding that slide's speaker notes.
23
+ */
24
+ parts: DocumentPart[];
25
+ metadata: Record<string, unknown>;
26
+ }
27
+ /** The Kreuzberg engine is not installed; nothing was read. */
28
+ export interface OfficeUnsupported extends UnsupportedReason {
29
+ status: 'unsupported';
30
+ format: OfficeFormat;
31
+ }
32
+ export type OfficeResult = OfficeExtraction | OfficeUnsupported;
33
+ /** @internal test hook: forget the cached engine lookup. */
34
+ export declare function resetOfficeEngineForTests(): void;
35
+ /**
36
+ * Read a DOCX or PPTX from memory (or a path) through Kreuzberg.
37
+ *
38
+ * Returns `{ status: 'unsupported' }` -- never throws for that case -- when the
39
+ * optional `@kreuzberg/node` dependency is not installed. Corrupt or
40
+ * unreadable files reject with the engine's error.
41
+ *
42
+ * @param source - File bytes or a local file path
43
+ * @param format - `'docx'` or `'pptx'`
44
+ */
45
+ export declare function extractOffice(source: Buffer | Uint8Array | string, format: OfficeFormat, options?: ExtractOfficeOptions): Promise<OfficeResult>;
46
+ declare abstract class OfficeProcessor implements DocumentProcessor {
47
+ protected abstract readonly format: OfficeFormat;
48
+ protected abstract readonly label: string;
49
+ abstract supports(type: string): boolean;
50
+ process(url: string, options?: FetchDocumentOptions): Promise<Document>;
51
+ }
52
+ /**
53
+ * Word (`.docx`) processor backed by Kreuzberg.
54
+ *
55
+ * Splits the text into heading-delimited parts (markdown). When the optional
56
+ * `@kreuzberg/node` package is missing, `process()` resolves with
57
+ * `document.unsupported` set and no parts.
58
+ */
59
+ export declare class DOCXProcessor extends OfficeProcessor {
60
+ protected readonly format: "docx";
61
+ protected readonly label = "Word Document";
62
+ supports(type: string): boolean;
63
+ }
64
+ /**
65
+ * PowerPoint (`.pptx`) processor backed by Kreuzberg.
66
+ *
67
+ * One part per slide, with `notes` carrying the speaker notes and `images`
68
+ * the pictures embedded on that slide. When the optional `@kreuzberg/node`
69
+ * package is missing, `process()` resolves with `document.unsupported` set.
70
+ */
71
+ export declare class PPTXProcessor extends OfficeProcessor {
72
+ protected readonly format: "pptx";
73
+ protected readonly label = "Presentation";
74
+ supports(type: string): boolean;
75
+ }
76
+ export {};
77
+ //# sourceMappingURL=office.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"office.d.ts","sourceRoot":"","sources":["../../src/processors/office.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,QAAQ,EAER,YAAY,EACZ,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,aAAU,CAAC;AAGlB,6CAA6C;AAC7C,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAC;AAE3C,eAAO,MAAM,SAAS,4EACqD,CAAC;AAC5E,eAAO,MAAM,SAAS,8EACuD,CAAC;AAS9E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACnC,8DAA8D;IAC9D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,+BAA+B;AAC/B,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,IAAI,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,+DAA+D;AAC/D,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,MAAM,EAAE,aAAa,CAAC;IACtB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,MAAM,YAAY,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAqChE,4DAA4D;AAC5D,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AA0JD;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,EACpC,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,YAAY,CAAC,CAiEvB;AAED,uBAAe,eAAgB,YAAW,iBAAiB;IACzD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IACjD,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAE1C,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAElC,OAAO,CACX,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,QAAQ,CAAC;CAuDrB;AAED;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,eAAe;IAChD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAG,MAAM,CAAU;IAC5C,SAAS,CAAC,QAAQ,CAAC,KAAK,mBAAmB;IAE3C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAIhC;AAED;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,eAAe;IAChD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAG,MAAM,CAAU;IAC5C,SAAS,CAAC,QAAQ,CAAC,KAAK,kBAAkB;IAE1C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAIhC"}
package/dist/types.d.ts CHANGED
@@ -20,6 +20,11 @@ export interface DocumentImage {
20
20
  * Local filesystem path if image has been downloaded
21
21
  */
22
22
  localPath?: string;
23
+ /**
24
+ * Raw image bytes when the extractor returned them in memory
25
+ * (Office documents). `url` is then a synthetic `office-image:` reference.
26
+ */
27
+ data?: Uint8Array;
23
28
  /**
24
29
  * Alt text from HTML or PDF metadata
25
30
  */
@@ -70,6 +75,10 @@ export interface DocumentPart {
70
75
  * Images contained in this part
71
76
  */
72
77
  images?: DocumentImage[];
78
+ /**
79
+ * Speaker notes for this part (PPTX slides)
80
+ */
81
+ notes?: string;
73
82
  /**
74
83
  * Additional metadata for this part
75
84
  */
@@ -99,6 +108,24 @@ export interface Document {
99
108
  * Document-level metadata
100
109
  */
101
110
  metadata?: Record<string, any>;
111
+ /**
112
+ * Set when this processor cannot read the document in the current
113
+ * environment (for example the optional `@kreuzberg/node` engine is not
114
+ * installed). `parts` is empty; callers should degrade gracefully rather
115
+ * than treat it as a failure of the file.
116
+ */
117
+ unsupported?: UnsupportedReason;
118
+ }
119
+ /**
120
+ * Why a document could not be processed.
121
+ */
122
+ export interface UnsupportedReason {
123
+ /** Machine-readable code. */
124
+ code: 'dependency-missing';
125
+ /** Human-readable explanation. */
126
+ reason: string;
127
+ /** The missing optional dependency, when applicable. */
128
+ dependency?: string;
102
129
  }
103
130
  /**
104
131
  * Options for fetching a document
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;CACH;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;IAEnC;;OAEG;IACH,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC;IAEzB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE/B;;OAEG;IACH,KAAK,CAAC,EAAE,YAAY,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,KAAK,EAAE,YAAY,EAAE,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAEvC;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,UAAU,CAAC;IAEnD;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,UAAU,CAAC;IAE1D;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAExE;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/metadata.json CHANGED
@@ -20,7 +20,9 @@
20
20
  "@happyvertical/pdf",
21
21
  "@happyvertical/spider"
22
22
  ],
23
- "external": []
23
+ "external": [
24
+ "@kreuzberg/node"
25
+ ]
24
26
  },
25
27
  "dependents": [],
26
28
  "stability": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@happyvertical/documents",
3
- "version": "0.111.3",
4
- "description": "Multi-part document processing with support for PDF, HTML, and Markdown",
3
+ "version": "0.111.4",
4
+ "description": "Multi-part document processing with support for PDF, Word, PowerPoint, HTML, and Markdown",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -36,11 +36,14 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@happyvertical/files": "0.111.3",
39
+ "@happyvertical/files": "0.111.4",
40
40
  "@happyvertical/ocr": "^0.61.1",
41
41
  "@happyvertical/pdf": "^0.65.9",
42
42
  "@happyvertical/spider": "^1.1",
43
- "@happyvertical/utils": "0.111.3"
43
+ "@happyvertical/utils": "0.111.4"
44
+ },
45
+ "optionalDependencies": {
46
+ "@kreuzberg/node": "^4.1.1"
44
47
  },
45
48
  "devDependencies": {
46
49
  "@types/node": "25.0.10",
@@ -51,6 +54,8 @@
51
54
  "keywords": [
52
55
  "documents",
53
56
  "pdf",
57
+ "docx",
58
+ "pptx",
54
59
  "html",
55
60
  "markdown",
56
61
  "ocr",