doc2vec 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,725 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ContentProcessor = void 0;
40
+ const readability_1 = require("@mozilla/readability");
41
+ const axios_1 = __importDefault(require("axios"));
42
+ const cheerio_1 = require("cheerio");
43
+ const jsdom_1 = require("jsdom");
44
+ const puppeteer_1 = __importDefault(require("puppeteer"));
45
+ const sanitize_html_1 = __importDefault(require("sanitize-html"));
46
+ const turndown_1 = __importDefault(require("turndown"));
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const utils_1 = require("./utils");
50
+ class ContentProcessor {
51
+ constructor(logger) {
52
+ this.logger = logger;
53
+ this.turndownService = new turndown_1.default({
54
+ codeBlockStyle: 'fenced',
55
+ headingStyle: 'atx'
56
+ });
57
+ this.setupTurndownRules();
58
+ }
59
+ setupTurndownRules() {
60
+ const logger = this.logger.child('markdown');
61
+ logger.debug('Setting up Turndown rules for markdown conversion');
62
+ this.turndownService.addRule('codeBlocks', {
63
+ filter: (node) => node.nodeName === 'PRE',
64
+ replacement: (content, node) => {
65
+ const htmlNode = node;
66
+ const code = htmlNode.querySelector('code');
67
+ let codeContent;
68
+ if (code) {
69
+ codeContent = code.textContent || '';
70
+ }
71
+ else {
72
+ codeContent = htmlNode.textContent || '';
73
+ }
74
+ const lines = codeContent.split('\n');
75
+ let minIndent = Infinity;
76
+ for (const line of lines) {
77
+ if (line.trim() === '')
78
+ continue;
79
+ const leadingWhitespace = line.match(/^\s*/)?.[0] || '';
80
+ minIndent = Math.min(minIndent, leadingWhitespace.length);
81
+ }
82
+ const cleanedLines = lines.map(line => {
83
+ return line.substring(minIndent);
84
+ });
85
+ let cleanContent = cleanedLines.join('\n');
86
+ cleanContent = cleanContent.replace(/^\s+|\s+$/g, '');
87
+ cleanContent = cleanContent.replace(/\n{2,}/g, '\n');
88
+ return `\n\`\`\`\n${cleanContent}\n\`\`\`\n`;
89
+ }
90
+ });
91
+ this.turndownService.addRule('tableCell', {
92
+ filter: ['th', 'td'],
93
+ replacement: (content, node) => {
94
+ const htmlNode = node;
95
+ let cellContent = '';
96
+ if (htmlNode.querySelector('p')) {
97
+ cellContent = Array.from(htmlNode.querySelectorAll('p'))
98
+ .map(p => p.textContent || '')
99
+ .join(' ')
100
+ .trim();
101
+ }
102
+ else {
103
+ cellContent = content.trim();
104
+ }
105
+ return ` ${cellContent.replace(/\|/g, '\\|')} |`;
106
+ }
107
+ });
108
+ this.turndownService.addRule('tableRow', {
109
+ filter: 'tr',
110
+ replacement: (content, node) => {
111
+ const htmlNode = node;
112
+ const cells = Array.from(htmlNode.cells);
113
+ const isHeader = htmlNode.parentNode?.nodeName === 'THEAD';
114
+ let output = '|' + content.trimEnd();
115
+ if (isHeader) {
116
+ const separator = cells.map(() => '---').join(' | ');
117
+ output += '\n|' + separator + '|';
118
+ }
119
+ if (!isHeader || !htmlNode.nextElementSibling) {
120
+ output += '\n';
121
+ }
122
+ return output;
123
+ }
124
+ });
125
+ this.turndownService.addRule('table', {
126
+ filter: 'table',
127
+ replacement: (content) => {
128
+ return '\n' + content.replace(/\n+/g, '\n').trim() + '\n';
129
+ }
130
+ });
131
+ this.turndownService.addRule('preserveTableWhitespace', {
132
+ filter: (node) => {
133
+ return ((node.nodeName === 'TD' || node.nodeName === 'TH') &&
134
+ (node.textContent?.trim().length === 0));
135
+ },
136
+ replacement: () => {
137
+ return ' |';
138
+ }
139
+ });
140
+ logger.debug('Turndown rules setup complete');
141
+ }
142
+ async parseSitemap(sitemapUrl, logger) {
143
+ logger.info(`Parsing sitemap from ${sitemapUrl}`);
144
+ try {
145
+ const response = await axios_1.default.get(sitemapUrl);
146
+ const $ = (0, cheerio_1.load)(response.data, { xmlMode: true });
147
+ const urls = [];
148
+ // Handle standard sitemaps
149
+ $('url > loc').each((_, element) => {
150
+ const url = $(element).text().trim();
151
+ if (url) {
152
+ urls.push(url);
153
+ }
154
+ });
155
+ // Handle sitemap indexes (sitemaps that link to other sitemaps)
156
+ const sitemapLinks = [];
157
+ $('sitemap > loc').each((_, element) => {
158
+ const nestedSitemapUrl = $(element).text().trim();
159
+ if (nestedSitemapUrl) {
160
+ sitemapLinks.push(nestedSitemapUrl);
161
+ }
162
+ });
163
+ // Recursively process nested sitemaps
164
+ for (const nestedSitemapUrl of sitemapLinks) {
165
+ logger.debug(`Found nested sitemap: ${nestedSitemapUrl}`);
166
+ const nestedUrls = await this.parseSitemap(nestedSitemapUrl, logger);
167
+ urls.push(...nestedUrls);
168
+ }
169
+ logger.info(`Found ${urls.length} URLs in sitemap ${sitemapUrl}`);
170
+ return urls;
171
+ }
172
+ catch (error) {
173
+ logger.error(`Error parsing sitemap at ${sitemapUrl}:`, error);
174
+ return [];
175
+ }
176
+ }
177
+ async crawlWebsite(baseUrl, sourceConfig, processPageContent, parentLogger, visitedUrls) {
178
+ const logger = parentLogger.child('crawler');
179
+ const queue = [baseUrl];
180
+ // Process sitemap if provided
181
+ if (sourceConfig.sitemap_url) {
182
+ logger.section('SITEMAP PROCESSING');
183
+ const sitemapUrls = await this.parseSitemap(sourceConfig.sitemap_url, logger);
184
+ // Add sitemap URLs to the queue if they're within the website scope
185
+ for (const url of sitemapUrls) {
186
+ if (url.startsWith(sourceConfig.url) && !queue.includes(url)) {
187
+ logger.debug(`Adding URL from sitemap to queue: ${url}`);
188
+ queue.push(url);
189
+ }
190
+ }
191
+ logger.info(`Added ${queue.length - 1} URLs from sitemap to the crawl queue`);
192
+ }
193
+ logger.info(`Starting crawl from ${baseUrl} with ${queue.length} URLs in initial queue`);
194
+ let processedCount = 0;
195
+ let skippedCount = 0;
196
+ let skippedSizeCount = 0;
197
+ let pdfProcessedCount = 0;
198
+ let errorCount = 0;
199
+ let hasNetworkErrors = false;
200
+ while (queue.length > 0) {
201
+ const url = queue.shift();
202
+ if (!url)
203
+ continue;
204
+ const normalizedUrl = utils_1.Utils.normalizeUrl(url);
205
+ if (visitedUrls.has(normalizedUrl))
206
+ continue;
207
+ visitedUrls.add(normalizedUrl);
208
+ if (!utils_1.Utils.shouldProcessUrl(url)) {
209
+ logger.debug(`Skipping URL with unsupported extension: ${url}`);
210
+ skippedCount++;
211
+ continue;
212
+ }
213
+ try {
214
+ logger.info(`Crawling: ${url}`);
215
+ const content = await this.processPage(url, sourceConfig);
216
+ if (content !== null) {
217
+ await processPageContent(url, content);
218
+ if (utils_1.Utils.isPdfUrl(url)) {
219
+ pdfProcessedCount++;
220
+ }
221
+ else {
222
+ processedCount++;
223
+ }
224
+ }
225
+ else {
226
+ skippedSizeCount++;
227
+ }
228
+ // Only try to extract links from HTML pages, not PDFs
229
+ if (!utils_1.Utils.isPdfUrl(url)) {
230
+ const response = await axios_1.default.get(url);
231
+ const $ = (0, cheerio_1.load)(response.data);
232
+ logger.debug(`Finding links on page ${url}`);
233
+ let newLinksFound = 0;
234
+ $('a[href]').each((_, element) => {
235
+ const href = $(element).attr('href');
236
+ if (!href || href.startsWith('#') || href.startsWith('mailto:'))
237
+ return;
238
+ const fullUrl = utils_1.Utils.buildUrl(href, url);
239
+ if (fullUrl.startsWith(sourceConfig.url) && !visitedUrls.has(utils_1.Utils.normalizeUrl(fullUrl))) {
240
+ if (!queue.includes(fullUrl)) {
241
+ queue.push(fullUrl);
242
+ newLinksFound++;
243
+ }
244
+ }
245
+ });
246
+ logger.debug(`Found ${newLinksFound} new links on ${url}`);
247
+ }
248
+ }
249
+ catch (error) {
250
+ logger.error(`Failed during processing or link discovery for ${url}:`, error);
251
+ errorCount++;
252
+ // Check if this is a network error (DNS resolution, connection issues, etc.)
253
+ if (this.isNetworkError(error)) {
254
+ hasNetworkErrors = true;
255
+ logger.warn(`Network error detected for ${url}, this may affect cleanup decisions`);
256
+ }
257
+ }
258
+ }
259
+ logger.info(`Crawl completed. HTML Pages: ${processedCount}, PDFs: ${pdfProcessedCount}, Skipped (Extension): ${skippedCount}, Skipped (Size): ${skippedSizeCount}, Errors: ${errorCount}`);
260
+ if (hasNetworkErrors) {
261
+ logger.warn('Network errors were encountered during crawling. Cleanup may be skipped to avoid removing valid chunks.');
262
+ }
263
+ return { hasNetworkErrors };
264
+ }
265
+ isNetworkError(error) {
266
+ // Check for common network error patterns
267
+ if (error?.code) {
268
+ // DNS resolution errors
269
+ if (error.code === 'ENOTFOUND')
270
+ return true;
271
+ // Connection refused
272
+ if (error.code === 'ECONNREFUSED')
273
+ return true;
274
+ // Connection timeout
275
+ if (error.code === 'ETIMEDOUT')
276
+ return true;
277
+ // Connection reset
278
+ if (error.code === 'ECONNRESET')
279
+ return true;
280
+ // Host unreachable
281
+ if (error.code === 'EHOSTUNREACH')
282
+ return true;
283
+ // Network unreachable
284
+ if (error.code === 'ENETUNREACH')
285
+ return true;
286
+ }
287
+ // Check for axios-specific network errors
288
+ if (error?.isAxiosError) {
289
+ // If there's no response, it's likely a network error
290
+ if (!error.response)
291
+ return true;
292
+ }
293
+ // Check error message for network-related terms
294
+ const errorMessage = error?.message?.toLowerCase() || '';
295
+ if (errorMessage.includes('getaddrinfo') ||
296
+ errorMessage.includes('network') ||
297
+ errorMessage.includes('timeout') ||
298
+ errorMessage.includes('connection') ||
299
+ errorMessage.includes('dns')) {
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+ async processPage(url, sourceConfig) {
305
+ const logger = this.logger.child('page-processor');
306
+ logger.debug(`Processing content from ${url}`);
307
+ // Check if this is a PDF URL
308
+ if (utils_1.Utils.isPdfUrl(url)) {
309
+ logger.info(`Processing PDF: ${url}`);
310
+ try {
311
+ const markdown = await this.downloadAndConvertPdfFromUrl(url, logger);
312
+ // Check size limit for PDF content
313
+ if (markdown.length > sourceConfig.max_size) {
314
+ logger.warn(`PDF content (${markdown.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping ${url}.`);
315
+ return null;
316
+ }
317
+ return markdown;
318
+ }
319
+ catch (error) {
320
+ logger.error(`Failed to process PDF ${url}:`, error);
321
+ return null;
322
+ }
323
+ }
324
+ // Original HTML page processing logic
325
+ let browser = null;
326
+ try {
327
+ browser = await puppeteer_1.default.launch({
328
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
329
+ });
330
+ const page = await browser.newPage();
331
+ logger.debug(`Navigating to ${url}`);
332
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
333
+ const htmlContent = await page.evaluate(() => {
334
+ const mainContentElement = document.querySelector('div[role="main"].document') || document.querySelector('main') || document.body;
335
+ return mainContentElement.innerHTML;
336
+ });
337
+ if (htmlContent.length > sourceConfig.max_size) {
338
+ logger.warn(`Raw HTML content (${htmlContent.length} chars) exceeds max size (${sourceConfig.max_size}). Skipping detailed processing for ${url}.`);
339
+ await browser.close();
340
+ return null;
341
+ }
342
+ logger.debug(`Got HTML content (${htmlContent.length} chars), creating DOM`);
343
+ const dom = new jsdom_1.JSDOM(htmlContent);
344
+ const document = dom.window.document;
345
+ document.querySelectorAll('pre').forEach((pre) => {
346
+ pre.classList.add('article-content');
347
+ pre.setAttribute('data-readable-content-score', '100');
348
+ this.markCodeParents(pre.parentElement);
349
+ });
350
+ logger.debug(`Applying Readability to extract main content`);
351
+ const reader = new readability_1.Readability(document, {
352
+ charThreshold: 20,
353
+ classesToPreserve: ['article-content'],
354
+ });
355
+ const article = reader.parse();
356
+ if (!article) {
357
+ logger.warn(`Failed to parse article content with Readability for ${url}`);
358
+ await browser.close();
359
+ return null;
360
+ }
361
+ logger.debug(`Sanitizing HTML (${article.content.length} chars)`);
362
+ const cleanHtml = (0, sanitize_html_1.default)(article.content, {
363
+ allowedTags: [
364
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
365
+ 'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
366
+ 'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
367
+ ],
368
+ allowedAttributes: {
369
+ 'a': ['href'],
370
+ 'pre': ['class', 'data-language'],
371
+ 'code': ['class', 'data-language'],
372
+ 'div': ['class'],
373
+ 'span': ['class']
374
+ }
375
+ });
376
+ logger.debug(`Converting HTML to Markdown`);
377
+ const markdown = this.turndownService.turndown(cleanHtml);
378
+ logger.debug(`Markdown conversion complete (${markdown.length} chars)`);
379
+ return markdown;
380
+ }
381
+ catch (error) {
382
+ logger.error(`Error processing page ${url}:`, error);
383
+ return null;
384
+ }
385
+ finally {
386
+ if (browser && browser.isConnected()) {
387
+ await browser.close();
388
+ logger.debug(`Browser closed for ${url}`);
389
+ }
390
+ }
391
+ }
392
+ markCodeParents(node) {
393
+ if (!node)
394
+ return;
395
+ if (node.querySelector('pre, code')) {
396
+ node.classList.add('article-content');
397
+ node.setAttribute('data-readable-content-score', '100');
398
+ }
399
+ this.markCodeParents(node.parentElement);
400
+ }
401
+ async convertPdfToMarkdown(filePath, logger) {
402
+ logger.debug(`Converting PDF to markdown: ${filePath}`);
403
+ try {
404
+ // Dynamic import for PDF.js to handle ES module compatibility
405
+ const pdfjsLib = await Promise.resolve().then(() => __importStar(require('pdfjs-dist/legacy/build/pdf.mjs')));
406
+ // Read the PDF file as a buffer and convert to Uint8Array
407
+ const pdfBuffer = fs.readFileSync(filePath);
408
+ const pdfData = new Uint8Array(pdfBuffer);
409
+ // Load the PDF document
410
+ const loadingTask = pdfjsLib.getDocument({
411
+ data: pdfData,
412
+ // Disable worker to avoid issues in Node.js environment
413
+ useWorkerFetch: false,
414
+ isEvalSupported: false,
415
+ useSystemFonts: true
416
+ });
417
+ const pdfDocument = await loadingTask.promise;
418
+ const numPages = pdfDocument.numPages;
419
+ logger.debug(`PDF has ${numPages} pages`);
420
+ let markdown = `# ${path.basename(filePath, '.pdf')}\n\n`;
421
+ // Extract text from each page
422
+ for (let pageNum = 1; pageNum <= numPages; pageNum++) {
423
+ const page = await pdfDocument.getPage(pageNum);
424
+ const textContent = await page.getTextContent();
425
+ // Combine text items into a readable format
426
+ let pageText = '';
427
+ let currentY = -1;
428
+ for (const item of textContent.items) {
429
+ if ('str' in item) {
430
+ // If this is a new line (different Y position), add a line break
431
+ if (currentY !== -1 && Math.abs(item.transform[5] - currentY) > 5) {
432
+ pageText += '\n';
433
+ }
434
+ pageText += item.str;
435
+ // Add space if the next item doesn't start immediately after this one
436
+ if ('width' in item && item.width > 0) {
437
+ pageText += ' ';
438
+ }
439
+ currentY = item.transform[5];
440
+ }
441
+ }
442
+ // Clean up the text
443
+ pageText = pageText
444
+ .replace(/\s+/g, ' ') // Replace multiple whitespace with single space
445
+ .replace(/\n\s+/g, '\n') // Clean up line starts
446
+ .trim();
447
+ if (pageText.length > 0) {
448
+ if (numPages > 1) {
449
+ markdown += `## Page ${pageNum}\n\n`;
450
+ }
451
+ markdown += pageText + '\n\n';
452
+ }
453
+ }
454
+ // Clean up the final markdown
455
+ markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
456
+ logger.debug(`Converted PDF to ${markdown.length} characters of markdown`);
457
+ return markdown;
458
+ }
459
+ catch (error) {
460
+ logger.error(`Failed to convert PDF ${filePath}:`, error);
461
+ throw error;
462
+ }
463
+ }
464
+ async downloadAndConvertPdfFromUrl(url, logger) {
465
+ logger.debug(`Downloading and converting PDF from URL: ${url}`);
466
+ try {
467
+ // Download the PDF file
468
+ const response = await axios_1.default.get(url, {
469
+ responseType: 'arraybuffer',
470
+ timeout: 60000, // 60 second timeout
471
+ headers: {
472
+ 'User-Agent': 'Mozilla/5.0 (compatible; doc2vec PDF processor)'
473
+ }
474
+ });
475
+ if (response.status !== 200) {
476
+ throw new Error(`Failed to download PDF: HTTP ${response.status}`);
477
+ }
478
+ logger.debug(`Downloaded PDF (${response.data.byteLength} bytes)`);
479
+ // Dynamic import for PDF.js to handle ES module compatibility
480
+ const pdfjsLib = await Promise.resolve().then(() => __importStar(require('pdfjs-dist/legacy/build/pdf.mjs')));
481
+ // Convert ArrayBuffer to Uint8Array
482
+ const pdfData = new Uint8Array(response.data);
483
+ // Load the PDF document
484
+ const loadingTask = pdfjsLib.getDocument({
485
+ data: pdfData,
486
+ // Disable worker to avoid issues in Node.js environment
487
+ useWorkerFetch: false,
488
+ isEvalSupported: false,
489
+ useSystemFonts: true
490
+ });
491
+ const pdfDocument = await loadingTask.promise;
492
+ const numPages = pdfDocument.numPages;
493
+ logger.debug(`PDF has ${numPages} pages`);
494
+ // Get the filename from URL for the title
495
+ const urlPath = new URL(url).pathname;
496
+ const filename = path.basename(urlPath, '.pdf') || 'document';
497
+ let markdown = `# ${filename}\n\n`;
498
+ // Extract text from each page
499
+ for (let pageNum = 1; pageNum <= numPages; pageNum++) {
500
+ const page = await pdfDocument.getPage(pageNum);
501
+ const textContent = await page.getTextContent();
502
+ // Combine text items into a readable format
503
+ let pageText = '';
504
+ let currentY = -1;
505
+ for (const item of textContent.items) {
506
+ if ('str' in item) {
507
+ // If this is a new line (different Y position), add a line break
508
+ if (currentY !== -1 && Math.abs(item.transform[5] - currentY) > 5) {
509
+ pageText += '\n';
510
+ }
511
+ pageText += item.str;
512
+ // Add space if the next item doesn't start immediately after this one
513
+ if ('width' in item && item.width > 0) {
514
+ pageText += ' ';
515
+ }
516
+ currentY = item.transform[5];
517
+ }
518
+ }
519
+ // Clean up the text
520
+ pageText = pageText
521
+ .replace(/\s+/g, ' ') // Replace multiple whitespace with single space
522
+ .replace(/\n\s+/g, '\n') // Clean up line starts
523
+ .trim();
524
+ if (pageText.length > 0) {
525
+ if (numPages > 1) {
526
+ markdown += `## Page ${pageNum}\n\n`;
527
+ }
528
+ markdown += pageText + '\n\n';
529
+ }
530
+ }
531
+ // Clean up the final markdown
532
+ markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
533
+ logger.debug(`Converted PDF to ${markdown.length} characters of markdown`);
534
+ return markdown;
535
+ }
536
+ catch (error) {
537
+ logger.error(`Failed to download and convert PDF from ${url}:`, error);
538
+ throw error;
539
+ }
540
+ }
541
+ async processDirectory(dirPath, config, processFileContent, parentLogger, visitedPaths = new Set()) {
542
+ const logger = parentLogger.child('directory-processor');
543
+ logger.info(`Processing directory: ${dirPath}`);
544
+ const recursive = config.recursive !== undefined ? config.recursive : true;
545
+ const includeExtensions = config.include_extensions || ['.md', '.txt', '.html', '.htm', '.pdf'];
546
+ const excludeExtensions = config.exclude_extensions || [];
547
+ const encoding = config.encoding || 'utf8';
548
+ try {
549
+ const files = fs.readdirSync(dirPath);
550
+ let processedFiles = 0;
551
+ let skippedFiles = 0;
552
+ for (const file of files) {
553
+ const filePath = path.join(dirPath, file);
554
+ const stat = fs.statSync(filePath);
555
+ // Skip already visited paths
556
+ if (visitedPaths.has(filePath)) {
557
+ logger.debug(`Skipping already visited path: ${filePath}`);
558
+ continue;
559
+ }
560
+ visitedPaths.add(filePath);
561
+ if (stat.isDirectory()) {
562
+ if (recursive) {
563
+ await this.processDirectory(filePath, config, processFileContent, logger, visitedPaths);
564
+ }
565
+ else {
566
+ logger.debug(`Skipping directory ${filePath} (recursive=false)`);
567
+ }
568
+ }
569
+ else if (stat.isFile()) {
570
+ const extension = path.extname(file).toLowerCase();
571
+ // Apply extension filters
572
+ if (excludeExtensions.includes(extension)) {
573
+ logger.debug(`Skipping file with excluded extension: ${filePath}`);
574
+ skippedFiles++;
575
+ continue;
576
+ }
577
+ if (includeExtensions.length > 0 && !includeExtensions.includes(extension)) {
578
+ logger.debug(`Skipping file with non-included extension: ${filePath}`);
579
+ skippedFiles++;
580
+ continue;
581
+ }
582
+ try {
583
+ logger.info(`Reading file: ${filePath}`);
584
+ let content;
585
+ let processedContent;
586
+ if (extension === '.pdf') {
587
+ // Handle PDF files
588
+ logger.debug(`Processing PDF file: ${filePath}`);
589
+ processedContent = await this.convertPdfToMarkdown(filePath, logger);
590
+ }
591
+ else {
592
+ // Handle text-based files
593
+ content = fs.readFileSync(filePath, { encoding: encoding });
594
+ if (content.length > config.max_size) {
595
+ logger.warn(`File content (${content.length} chars) exceeds max size (${config.max_size}). Skipping ${filePath}.`);
596
+ skippedFiles++;
597
+ continue;
598
+ }
599
+ // Convert HTML to Markdown if needed
600
+ if (extension === '.html' || extension === '.htm') {
601
+ logger.debug(`Converting HTML to Markdown for ${filePath}`);
602
+ const cleanHtml = (0, sanitize_html_1.default)(content, {
603
+ allowedTags: [
604
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
605
+ 'li', 'b', 'i', 'strong', 'em', 'code', 'pre',
606
+ 'div', 'span', 'table', 'thead', 'tbody', 'tr', 'th', 'td'
607
+ ],
608
+ allowedAttributes: {
609
+ 'a': ['href'],
610
+ 'pre': ['class', 'data-language'],
611
+ 'code': ['class', 'data-language'],
612
+ 'div': ['class'],
613
+ 'span': ['class']
614
+ }
615
+ });
616
+ processedContent = this.turndownService.turndown(cleanHtml);
617
+ }
618
+ else {
619
+ processedContent = content;
620
+ }
621
+ }
622
+ // Check size limit for processed content
623
+ if (processedContent.length > config.max_size) {
624
+ logger.warn(`Processed content (${processedContent.length} chars) exceeds max size (${config.max_size}). Skipping ${filePath}.`);
625
+ skippedFiles++;
626
+ continue;
627
+ }
628
+ await processFileContent(filePath, processedContent);
629
+ processedFiles++;
630
+ }
631
+ catch (error) {
632
+ logger.error(`Error processing file ${filePath}:`, error);
633
+ }
634
+ }
635
+ }
636
+ logger.info(`Directory processed. Processed: ${processedFiles}, Skipped: ${skippedFiles}`);
637
+ }
638
+ catch (error) {
639
+ logger.error(`Error reading directory ${dirPath}:`, error);
640
+ }
641
+ }
642
+ async chunkMarkdown(markdown, sourceConfig, url) {
643
+ const logger = this.logger.child('chunker');
644
+ logger.debug(`Chunking markdown from ${url} (${markdown.length} chars)`);
645
+ const MAX_TOKENS = 1000;
646
+ const chunks = [];
647
+ const lines = markdown.split("\n");
648
+ let currentChunk = "";
649
+ let headingHierarchy = [];
650
+ const processChunk = () => {
651
+ if (currentChunk.trim()) {
652
+ const tokens = utils_1.Utils.tokenize(currentChunk);
653
+ if (tokens.length > MAX_TOKENS) {
654
+ logger.debug(`Chunk exceeds max token count (${tokens.length}), splitting into smaller chunks`);
655
+ let subChunk = "";
656
+ let tokenCount = 0;
657
+ const overlapSize = Math.floor(MAX_TOKENS * 0.05);
658
+ let lastTokens = [];
659
+ for (const token of tokens) {
660
+ if (tokenCount + 1 > MAX_TOKENS) {
661
+ chunks.push(createDocumentChunk(subChunk, headingHierarchy));
662
+ subChunk = lastTokens.join("") + token;
663
+ tokenCount = lastTokens.length + 1;
664
+ lastTokens = [];
665
+ }
666
+ else {
667
+ subChunk += token;
668
+ tokenCount++;
669
+ lastTokens.push(token);
670
+ if (lastTokens.length > overlapSize) {
671
+ lastTokens.shift();
672
+ }
673
+ }
674
+ }
675
+ if (subChunk) {
676
+ chunks.push(createDocumentChunk(subChunk, headingHierarchy));
677
+ }
678
+ }
679
+ else {
680
+ chunks.push(createDocumentChunk(currentChunk, headingHierarchy));
681
+ }
682
+ }
683
+ currentChunk = "";
684
+ };
685
+ const createDocumentChunk = (content, hierarchy) => {
686
+ const chunkId = utils_1.Utils.generateHash(content);
687
+ logger.debug(`Created chunk ${chunkId.substring(0, 8)}... with ${content.length} chars`);
688
+ return {
689
+ content,
690
+ metadata: {
691
+ product_name: sourceConfig.product_name,
692
+ version: sourceConfig.version,
693
+ heading_hierarchy: [...hierarchy],
694
+ section: hierarchy[hierarchy.length - 1] || "Introduction",
695
+ chunk_id: chunkId,
696
+ url: url,
697
+ hash: utils_1.Utils.generateHash(content)
698
+ }
699
+ };
700
+ };
701
+ for (const line of lines) {
702
+ if (line.startsWith("#")) {
703
+ processChunk();
704
+ const levelMatch = line.match(/^(#+)/);
705
+ let level = levelMatch ? levelMatch[1].length : 1;
706
+ const heading = line.replace(/^#+\s*/, "").trim();
707
+ logger.debug(`Found heading (level ${level}): ${heading}`);
708
+ while (headingHierarchy.length < level - 1) {
709
+ headingHierarchy.push("");
710
+ }
711
+ if (level <= headingHierarchy.length) {
712
+ headingHierarchy = headingHierarchy.slice(0, level - 1);
713
+ }
714
+ headingHierarchy[level - 1] = heading;
715
+ }
716
+ else {
717
+ currentChunk += `${line}\n`;
718
+ }
719
+ }
720
+ processChunk();
721
+ logger.debug(`Chunking complete, created ${chunks.length} chunks`);
722
+ return chunks;
723
+ }
724
+ }
725
+ exports.ContentProcessor = ContentProcessor;