file2md
Version:
A TypeScript library for converting various document types (PDF, DOCX, XLSX, PPTX, HWP, HWPX) into Markdown with image and layout preservation
267 lines • 11.5 kB
JavaScript
import JSZip from 'jszip';
import { parseStringPromise } from 'xml2js';
import { LayoutParser } from '../utils/layout-parser.js';
import { ParseError, InvalidFileError } from '../types/errors.js';
/**
* Parse XLSX buffer and convert to markdown with formatting preservation
*/
export async function parseXlsx(buffer, _imageExtractor, chartExtractor, options = {}) {
try {
const zip = await JSZip.loadAsync(buffer);
const layoutParser = new LayoutParser();
const sharedStrings = await getSharedStrings(zip);
const workbook = await getWorkbook(zip);
const styles = await getStyles(zip);
const worksheets = await getWorksheets(zip, workbook, styles, sharedStrings);
// Extract charts if enabled
const extractedCharts = options.extractCharts !== false
? await chartExtractor.extractChartsFromZip(zip, 'xl/')
: [];
let markdown = '';
let sheetCount = 0;
for (const [sheetName, sheetData] of worksheets) {
if (sheetData.rows && sheetData.rows.length > 0) {
sheetCount++;
markdown += `### ${sheetName}\n\n`;
markdown += layoutParser.parseAdvancedTable(sheetData, {
preserveAlignment: true,
showBorders: true,
preserveColors: true
});
markdown += '\n\n';
}
}
return {
markdown: markdown.trim(),
charts: extractedCharts.map(chart => chart.data),
sheetCount,
metadata: {
totalSheets: workbook.length,
processedSheets: sheetCount
}
};
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new ParseError('XLSX', message, error);
}
}
async function getSharedStrings(zip) {
const sharedStringsFile = zip.file('xl/sharedStrings.xml');
if (!sharedStringsFile)
return [];
try {
const xmlContent = await sharedStringsFile.async('string');
const result = await parseStringPromise(xmlContent);
const strings = [];
if (result.sst?.si) {
for (const si of result.sst.si) {
if (si.t?.[0]) {
strings.push(si.t[0]);
}
else if (si.r) {
let text = '';
for (const r of si.r) {
if (r.t?.[0]) {
text += r.t[0];
}
}
strings.push(text);
}
}
}
return strings;
}
catch (error) {
console.warn('Failed to parse shared strings:', error instanceof Error ? error.message : 'Unknown error');
return [];
}
}
async function getWorkbook(zip) {
const workbookFile = zip.file('xl/workbook.xml');
if (!workbookFile) {
throw new InvalidFileError('Invalid XLSX file: missing workbook.xml');
}
try {
const xmlContent = await workbookFile.async('string');
const result = await parseStringPromise(xmlContent);
const sheets = [];
if (result.workbook?.sheets?.[0]?.sheet) {
for (const sheet of result.workbook.sheets[0].sheet) {
sheets.push({
name: sheet.$.name,
sheetId: sheet.$.sheetId,
rId: sheet.$['r:id']
});
}
}
return sheets;
}
catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
throw new ParseError('XLSX', `Failed to parse workbook: ${message}`, error);
}
}
async function getStyles(zip) {
const stylesFile = zip.file('xl/styles.xml');
if (!stylesFile)
return { fonts: [], fills: [], cellXfs: [] };
try {
const xmlContent = await stylesFile.async('string');
const result = await parseStringPromise(xmlContent);
const styles = {
fonts: [],
fills: [],
cellXfs: []
};
// Parse fonts
if (result.styleSheet?.fonts?.[0]?.font) {
for (const font of result.styleSheet.fonts[0].font) {
const fontData = {
bold: !!(font.b?.[0]),
italic: !!(font.i?.[0]),
size: font.sz ? parseFloat(font.sz[0].$.val) : 11
};
styles.fonts.push(fontData);
}
}
// Parse fills (background colors)
if (result.styleSheet?.fills?.[0]?.fill) {
for (const fill of result.styleSheet.fills[0].fill) {
let bgColor;
if (fill.patternFill?.[0]?.bgColor?.[0]?.$.rgb) {
bgColor = fill.patternFill[0].bgColor[0].$.rgb;
}
styles.fills.push({ backgroundColor: bgColor });
}
}
// Parse cell formats
if (result.styleSheet?.cellXfs?.[0]?.xf) {
for (const xf of result.styleSheet.cellXfs[0].xf) {
const cellFormat = {
fontId: parseInt(xf.$.fontId, 10) || 0,
fillId: parseInt(xf.$.fillId, 10) || 0,
alignment: 'left'
};
if (xf.alignment?.[0]?.$.horizontal) {
cellFormat.alignment = xf.alignment[0].$.horizontal;
}
styles.cellXfs.push(cellFormat);
}
}
return styles;
}
catch (error) {
console.warn('Failed to parse styles:', error instanceof Error ? error.message : 'Unknown error');
return { fonts: [], fills: [], cellXfs: [] };
}
}
async function getWorksheets(zip, workbook, styles, sharedStrings) {
const worksheets = new Map();
for (let i = 0; i < workbook.length; i++) {
const sheet = workbook[i];
const worksheetFile = zip.file(`xl/worksheets/sheet${i + 1}.xml`);
if (worksheetFile) {
try {
const xmlContent = await worksheetFile.async('string');
const result = await parseStringPromise(xmlContent);
const sheetData = { rows: [] };
if (result.worksheet?.sheetData?.[0]?.row) {
const processedRows = [];
for (const row of result.worksheet.sheetData[0].row) {
const rowNum = parseInt(row.$.r, 10);
const rowData = { cells: [] };
if (row.c) {
const maxCol = Math.max(...row.c.map((cell) => getColumnIndex(cell.$.r)));
// Initialize all cells in the row
for (let col = 0; col <= maxCol; col++) {
rowData.cells.push({
text: '',
bold: false,
italic: false,
alignment: 'left',
backgroundColor: undefined,
colSpan: 1,
rowSpan: 1,
merged: false
});
}
// Fill in actual cell data
for (const cell of row.c) {
const cellRef = cell.$.r;
const colIndex = getColumnIndex(cellRef);
let cellValue = '';
if (cell.v?.[0]) {
cellValue = cell.v[0];
}
const cellData = {
text: cellValue,
bold: false,
italic: false,
alignment: 'left',
backgroundColor: undefined,
colSpan: 1,
rowSpan: 1,
merged: false
};
// Apply styling if available
if (cell.$.s && styles.cellXfs.length > 0) {
const styleIndex = parseInt(cell.$.s, 10);
const cellFormat = styles.cellXfs[styleIndex];
if (cellFormat) {
cellData.alignment = (['left', 'center', 'right', 'justify'].includes(cellFormat.alignment)
? cellFormat.alignment
: 'left');
// Apply font styling
if (styles.fonts[cellFormat.fontId]) {
const font = styles.fonts[cellFormat.fontId];
cellData.bold = font.bold;
cellData.italic = font.italic;
}
// Apply background color
if (styles.fills[cellFormat.fillId]) {
const fill = styles.fills[cellFormat.fillId];
cellData.backgroundColor = fill.backgroundColor;
}
}
}
// Handle shared strings
if (cell.$.t === 's' && cellValue) {
const stringIndex = parseInt(cellValue, 10);
if (stringIndex < sharedStrings.length) {
cellData.text = sharedStrings[stringIndex];
}
}
rowData.cells[colIndex] = cellData;
}
}
// Ensure the processedRows array is large enough
while (processedRows.length < rowNum) {
processedRows.push({ cells: [] });
}
processedRows[rowNum - 1] = rowData;
}
// Filter out empty rows
sheetData.rows = processedRows.filter(row => row.cells && row.cells.some(cell => cell.text && cell.text.trim()));
}
worksheets.set(sheet.name, sheetData);
}
catch (error) {
console.warn(`Failed to parse worksheet ${sheet.name}:`, error instanceof Error ? error.message : 'Unknown error');
}
}
}
return worksheets;
}
function getColumnIndex(cellRef) {
const match = cellRef.match(/^([A-Z]+)/);
if (!match)
return 0;
const letters = match[1];
let index = 0;
for (let i = 0; i < letters.length; i++) {
index = index * 26 + (letters.charCodeAt(i) - 64);
}
return index - 1;
}
//# sourceMappingURL=xlsx-parser.js.map