UNPKG

@wonderwhy-er/desktop-commander

Version:

MCP server for terminal operations and file editing

215 lines (214 loc) 8.22 kB
import path from 'path'; import fs from 'fs/promises'; import { capture } from '../utils.js'; // Direct imports import pdfParse from 'pdf-parse'; import TurndownService from 'turndown'; // PDF output formats export var PDFOutputFormat; (function (PDFOutputFormat) { PDFOutputFormat["TEXT"] = "text"; PDFOutputFormat["MARKDOWN"] = "markdown"; PDFOutputFormat["HTML"] = "html"; })(PDFOutputFormat || (PDFOutputFormat = {})); /** * Reads a PDF file and returns its content in the specified format * @param filePath Path to the PDF file * @param outputFormat Desired output format (text, markdown, or html) * @returns FileResult object with the parsed content */ export async function readPDFFile(filePath, outputFormat = PDFOutputFormat.TEXT) { try { // Read the PDF file as a buffer const dataBuffer = await fs.readFile(filePath); // Parse the PDF const pdfData = await pdfParse(dataBuffer); // Extract the raw text const rawText = pdfData.text; let content; let mimeType; // Convert to the requested format switch (outputFormat) { case PDFOutputFormat.HTML: // Convert to simple HTML (using paragraphs for line breaks) content = convertPDFToHTML(rawText, pdfData, filePath); mimeType = 'text/html'; break; case PDFOutputFormat.MARKDOWN: try { // First convert to HTML, then to Markdown const htmlContent = convertPDFToHTML(rawText, pdfData, filePath); const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', emDelimiter: '*' }); content = turndownService.turndown(htmlContent); mimeType = 'text/markdown'; } catch (error) { capture('server_turndown_error', { error: error instanceof Error ? error.message : String(error) }); // Fallback to basic markdown conversion if turndown has an issue content = convertPDFToBasicMarkdown(rawText, pdfData, filePath); mimeType = 'text/markdown'; } break; // First convert to HTML, then to Markdown const htmlContent = convertPDFToHTML(rawText, pdfData, filePath); const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', emDelimiter: '*' }); content = turndownService.turndown(htmlContent); mimeType = 'text/markdown'; break; case PDFOutputFormat.TEXT: default: // Format as readable text content = formatPDFText(rawText, pdfData, filePath); mimeType = 'text/plain'; break; } return { content, mimeType, isImage: false }; } catch (error) { capture('server_pdf_read_error', { error: error instanceof Error ? error.name : 'Unknown', errorMessage: error instanceof Error ? error.message : String(error) }); throw new Error(`Failed to read PDF file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Format PDF text to make it more readable as plain text * @param text Raw text from PDF * @param pdfData PDF data * @param filePath Original file path (for filename) * @returns Formatted text */ function formatPDFText(text, pdfData, filePath) { const fileName = path.basename(filePath); let output = `Document: ${fileName}\n`; output += `Pages: ${pdfData.numpages}\n\n`; // Add PDF metadata if available if (pdfData.info) { if (pdfData.info.Title) output += `Title: ${pdfData.info.Title}\n`; if (pdfData.info.Author) output += `Author: ${pdfData.info.Author}\n`; if (pdfData.info.Subject) output += `Subject: ${pdfData.info.Subject}\n`; if (pdfData.info.Keywords) output += `Keywords: ${pdfData.info.Keywords}\n`; if (pdfData.info.CreationDate) output += `Creation Date: ${pdfData.info.CreationDate}\n`; output += '\n'; } // Format the actual text content // Split by double line breaks to identify paragraphs const paragraphs = text.split(/\n\s*\n/); output += paragraphs.join('\n\n'); return output; } /** * Convert PDF text to basic markdown format without using turndown * @param text Raw text from PDF * @param pdfData PDF data * @param filePath Original file path (for filename) * @returns Markdown text */ function convertPDFToBasicMarkdown(text, pdfData, filePath) { const fileName = path.basename(filePath); let output = `# ${fileName}\n\n`; // Add PDF metadata if available if (pdfData.info) { if (pdfData.info.Title) output += `**Title:** ${pdfData.info.Title}\n\n`; if (pdfData.info.Author) output += `**Author:** ${pdfData.info.Author}\n\n`; if (pdfData.info.Subject) output += `**Subject:** ${pdfData.info.Subject}\n\n`; } // Split by double line breaks to identify paragraphs const paragraphs = text.split(/\n\s*\n/); // Process each paragraph paragraphs.forEach(paragraph => { const trimmed = paragraph.trim(); if (trimmed) { // Check if this looks like a heading (shorter, ends without punctuation) if (trimmed.length < 80 && !trimmed.match(/[.,:;?!]$/)) { output += `## ${trimmed}\n\n`; } else { output += `${trimmed}\n\n`; } } }); return output; } /** * Convert PDF text to a simple HTML representation * @param text Raw text from PDF * @param pdfData PDF data * @param filePath Original file path (for filename) * @returns HTML string */ function convertPDFToHTML(text, pdfData, filePath) { // This is a basic conversion that preserves paragraphs const fileName = path.basename(filePath); const title = pdfData.info && pdfData.info.Title ? pdfData.info.Title : fileName; // Split by double line breaks to identify paragraphs const paragraphs = text.split(/\n\s*\n/); // Build HTML structure let html = `<!DOCTYPE html> <html> <head> <title>${title}</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; } h1 { border-bottom: 1px solid #eee; padding-bottom: 10px; } h2 { margin-top: 24px; } pre { background-color: #f5f5f5; padding: 15px; border-radius: 5px; overflow-x: auto; } .metadata { color: #666; font-size: 0.9em; margin-bottom: 24px; } </style> </head> <body> <h1>${title}</h1> `; // Add metadata if available if (pdfData.info) { html += ' <div class="metadata">\n'; if (pdfData.info.Author) html += ` <p><strong>Author:</strong> ${pdfData.info.Author}</p>\n`; if (pdfData.info.Subject) html += ` <p><strong>Subject:</strong> ${pdfData.info.Subject}</p>\n`; if (pdfData.info.Keywords) html += ` <p><strong>Keywords:</strong> ${pdfData.info.Keywords}</p>\n`; if (pdfData.numpages) html += ` <p><strong>Pages:</strong> ${pdfData.numpages}</p>\n`; html += ' </div>\n'; } // Add paragraphs paragraphs.forEach(paragraph => { const trimmed = paragraph.trim(); if (trimmed) { // Check if this looks like a heading (shorter, ends without punctuation) if (trimmed.length < 100 && !trimmed.match(/[.,:;?!]$/)) { html += ` <h2>${trimmed}</h2>\n`; } else { html += ` <p>${trimmed}</p>\n`; } } }); html += `</body> </html>`; return html; }