Add support for csv + xlsx
This commit is contained in:
+253
-1
@@ -2,9 +2,10 @@ import fs from 'node:fs/promises';
|
||||
import mammoth from 'mammoth';
|
||||
import puppeteer from 'puppeteer';
|
||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs';
|
||||
import { Document, Paragraph, TextRun, HeadingLevel, Packer } from 'docx';
|
||||
import { Document, Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, Packer } from 'docx';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import TurndownService from 'turndown';
|
||||
import ExcelJS from 'exceljs';
|
||||
import { register } from './registry.js';
|
||||
|
||||
async function renderHtmlToPdf(html, outputPath) {
|
||||
@@ -217,6 +218,234 @@ async function convertPdfToDocx(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text) {
|
||||
return text
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function stripTags(html) {
|
||||
return decodeHtmlEntities(html.replace(/<[^>]*>/g, '')).trim();
|
||||
}
|
||||
|
||||
function parseHtmlTableToRows(html) {
|
||||
const tableMatch = html.match(/<table[^>]*>([\s\S]*?)<\/table>/i);
|
||||
if (!tableMatch) return [];
|
||||
const rowMatches = [...tableMatch[1].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)];
|
||||
return rowMatches.map((rowMatch) => {
|
||||
const cellMatches = [...rowMatch[1].matchAll(/<t[hd][^>]*>([\s\S]*?)<\/t[hd]>/gi)];
|
||||
return cellMatches.map((cellMatch) => stripTags(cellMatch[1]));
|
||||
});
|
||||
}
|
||||
|
||||
function rowsToHtmlDocument(rows) {
|
||||
const body = rows
|
||||
.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(String(cell ?? ''))}</td>`).join('')}</tr>`)
|
||||
.join('\n');
|
||||
return `<html><body><table>${body}</table></body></html>`;
|
||||
}
|
||||
|
||||
function escapeMarkdownCell(cell) {
|
||||
return String(cell ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
||||
}
|
||||
|
||||
function rowsToMarkdownTable(rows) {
|
||||
if (rows.length === 0) return '';
|
||||
const [header, ...body] = rows;
|
||||
const headerLine = `| ${header.map(escapeMarkdownCell).join(' | ')} |`;
|
||||
const separatorLine = `| ${header.map(() => '---').join(' | ')} |`;
|
||||
const bodyLines = body.map((row) => `| ${row.map(escapeMarkdownCell).join(' | ')} |`);
|
||||
return [headerLine, separatorLine, ...bodyLines].join('\n') + '\n';
|
||||
}
|
||||
|
||||
function inlineTokensToPlainText(children) {
|
||||
let text = '';
|
||||
for (const token of children ?? []) {
|
||||
if (token.type === 'text' || token.type === 'code_inline') text += token.content;
|
||||
else if (token.type === 'softbreak' || token.type === 'hardbreak') text += ' ';
|
||||
}
|
||||
return text.trim();
|
||||
}
|
||||
|
||||
function parseMarkdownTableToRows(markdown) {
|
||||
const tokens = new MarkdownIt().parse(markdown, {});
|
||||
const rows = [];
|
||||
let currentRow = null;
|
||||
for (let i = 0; i < tokens.length; i += 1) {
|
||||
const token = tokens[i];
|
||||
if (token.type === 'tr_open') {
|
||||
currentRow = [];
|
||||
} else if (token.type === 'tr_close') {
|
||||
if (currentRow) rows.push(currentRow);
|
||||
currentRow = null;
|
||||
} else if ((token.type === 'th_open' || token.type === 'td_open') && currentRow) {
|
||||
currentRow.push(inlineTokensToPlainText(tokens[i + 1]?.children));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function parseTxtToRows(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
||||
return lines.map((line) => line.split('\t'));
|
||||
}
|
||||
|
||||
function rowsToTxt(rows) {
|
||||
return `${rows.map((row) => row.map((cell) => String(cell ?? '')).join('\t')).join('\n')}\n`;
|
||||
}
|
||||
|
||||
async function parseDocxToRows(inputPath) {
|
||||
const result = await mammoth.convertToHtml({ path: inputPath });
|
||||
return parseHtmlTableToRows(result.value);
|
||||
}
|
||||
|
||||
async function writeRowsAsDocx(rows, outputPath) {
|
||||
const table = new Table({
|
||||
rows: rows.map(
|
||||
(row) =>
|
||||
new TableRow({
|
||||
children: row.map(
|
||||
(cell) => new TableCell({ children: [new Paragraph({ children: [new TextRun({ text: String(cell ?? '') })] })] })
|
||||
),
|
||||
})
|
||||
),
|
||||
});
|
||||
const doc = new Document({ sections: [{ children: [table] }] });
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
await fs.writeFile(outputPath, buffer);
|
||||
}
|
||||
|
||||
async function parsePdfToRows(inputPath) {
|
||||
const pageTexts = await extractPdfPageTexts(inputPath);
|
||||
return pageTexts.map((text) => [text]);
|
||||
}
|
||||
|
||||
async function readSpreadsheetRows(inputPath, format) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
let worksheet;
|
||||
if (format === 'csv') {
|
||||
worksheet = await workbook.csv.readFile(inputPath);
|
||||
} else {
|
||||
await workbook.xlsx.readFile(inputPath);
|
||||
[worksheet] = workbook.worksheets;
|
||||
}
|
||||
const rows = [];
|
||||
worksheet.eachRow({ includeEmpty: true }, (row) => {
|
||||
rows.push(row.values.slice(1).map((value) => value ?? ''));
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function writeSpreadsheetRows(rows, outputPath, format) {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const worksheet = workbook.addWorksheet('Sheet1');
|
||||
worksheet.addRows(rows);
|
||||
if (format === 'csv') {
|
||||
await workbook.csv.writeFile(outputPath);
|
||||
} else {
|
||||
await workbook.xlsx.writeFile(outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function convertCsvToXlsx(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await readSpreadsheetRows(inputPath, 'csv'), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
async function convertXlsxToCsv(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await readSpreadsheetRows(inputPath, 'xlsx'), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertCsvToHtml(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToHtmlDocument(await readSpreadsheetRows(inputPath, 'csv')));
|
||||
}
|
||||
|
||||
async function convertXlsxToHtml(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToHtmlDocument(await readSpreadsheetRows(inputPath, 'xlsx')));
|
||||
}
|
||||
|
||||
async function convertHtmlToCsv(inputPath, outputPath) {
|
||||
const html = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseHtmlTableToRows(html), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertHtmlToXlsx(inputPath, outputPath) {
|
||||
const html = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseHtmlTableToRows(html), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
async function convertCsvToMd(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToMarkdownTable(await readSpreadsheetRows(inputPath, 'csv')));
|
||||
}
|
||||
|
||||
async function convertXlsxToMd(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToMarkdownTable(await readSpreadsheetRows(inputPath, 'xlsx')));
|
||||
}
|
||||
|
||||
async function convertMdToCsv(inputPath, outputPath) {
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseMarkdownTableToRows(markdown), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertMdToXlsx(inputPath, outputPath) {
|
||||
const markdown = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseMarkdownTableToRows(markdown), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
async function convertCsvToTxt(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToTxt(await readSpreadsheetRows(inputPath, 'csv')));
|
||||
}
|
||||
|
||||
async function convertXlsxToTxt(inputPath, outputPath) {
|
||||
await fs.writeFile(outputPath, rowsToTxt(await readSpreadsheetRows(inputPath, 'xlsx')));
|
||||
}
|
||||
|
||||
async function convertTxtToCsv(inputPath, outputPath) {
|
||||
const text = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseTxtToRows(text), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertTxtToXlsx(inputPath, outputPath) {
|
||||
const text = await fs.readFile(inputPath, 'utf8');
|
||||
await writeSpreadsheetRows(parseTxtToRows(text), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
async function convertCsvToDocx(inputPath, outputPath) {
|
||||
await writeRowsAsDocx(await readSpreadsheetRows(inputPath, 'csv'), outputPath);
|
||||
}
|
||||
|
||||
async function convertXlsxToDocx(inputPath, outputPath) {
|
||||
await writeRowsAsDocx(await readSpreadsheetRows(inputPath, 'xlsx'), outputPath);
|
||||
}
|
||||
|
||||
async function convertDocxToCsv(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await parseDocxToRows(inputPath), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertDocxToXlsx(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await parseDocxToRows(inputPath), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
async function convertCsvToPdf(inputPath, outputPath) {
|
||||
await renderHtmlToPdf(rowsToHtmlDocument(await readSpreadsheetRows(inputPath, 'csv')), outputPath);
|
||||
}
|
||||
|
||||
async function convertXlsxToPdf(inputPath, outputPath) {
|
||||
await renderHtmlToPdf(rowsToHtmlDocument(await readSpreadsheetRows(inputPath, 'xlsx')), outputPath);
|
||||
}
|
||||
|
||||
async function convertPdfToCsv(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await parsePdfToRows(inputPath), outputPath, 'csv');
|
||||
}
|
||||
|
||||
async function convertPdfToXlsx(inputPath, outputPath) {
|
||||
await writeSpreadsheetRows(await parsePdfToRows(inputPath), outputPath, 'xlsx');
|
||||
}
|
||||
|
||||
export function registerDocumentConverters() {
|
||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'html', convert: convertDocxToHtml });
|
||||
register({ family: 'document', sourceFormat: 'txt', targetFormat: 'pdf', convert: convertTxtToPdf });
|
||||
@@ -233,4 +462,27 @@ export function registerDocumentConverters() {
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'txt', convert: convertPdfToTxt });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'html', convert: convertPdfToHtml });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'docx', convert: convertPdfToDocx });
|
||||
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'xlsx', convert: convertCsvToXlsx });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'csv', convert: convertXlsxToCsv });
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'html', convert: convertCsvToHtml });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'html', convert: convertXlsxToHtml });
|
||||
register({ family: 'document', sourceFormat: 'html', targetFormat: 'csv', convert: convertHtmlToCsv });
|
||||
register({ family: 'document', sourceFormat: 'html', targetFormat: 'xlsx', convert: convertHtmlToXlsx });
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'md', convert: convertCsvToMd });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'md', convert: convertXlsxToMd });
|
||||
register({ family: 'document', sourceFormat: 'md', targetFormat: 'csv', convert: convertMdToCsv });
|
||||
register({ family: 'document', sourceFormat: 'md', targetFormat: 'xlsx', convert: convertMdToXlsx });
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'txt', convert: convertCsvToTxt });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'txt', convert: convertXlsxToTxt });
|
||||
register({ family: 'document', sourceFormat: 'txt', targetFormat: 'csv', convert: convertTxtToCsv });
|
||||
register({ family: 'document', sourceFormat: 'txt', targetFormat: 'xlsx', convert: convertTxtToXlsx });
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'docx', convert: convertCsvToDocx });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'docx', convert: convertXlsxToDocx });
|
||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'csv', convert: convertDocxToCsv });
|
||||
register({ family: 'document', sourceFormat: 'docx', targetFormat: 'xlsx', convert: convertDocxToXlsx });
|
||||
register({ family: 'document', sourceFormat: 'csv', targetFormat: 'pdf', convert: convertCsvToPdf });
|
||||
register({ family: 'document', sourceFormat: 'xlsx', targetFormat: 'pdf', convert: convertXlsxToPdf });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'csv', convert: convertPdfToCsv });
|
||||
register({ family: 'document', sourceFormat: 'pdf', targetFormat: 'xlsx', convert: convertPdfToXlsx });
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ const OUTPUT_MIME_TYPES = {
|
||||
txt: 'text/plain',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
md: 'text/markdown',
|
||||
csv: 'text/csv',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
};
|
||||
|
||||
export async function detectInputMime(filePath) {
|
||||
@@ -33,6 +35,7 @@ const UNDETECTABLE_TEXT_FORMATS = {
|
||||
txt: 'text/plain',
|
||||
html: 'text/html',
|
||||
md: 'text/markdown',
|
||||
csv: 'text/csv',
|
||||
};
|
||||
|
||||
function normalizeFormat(format) {
|
||||
|
||||
Reference in New Issue
Block a user