diff --git a/src/generate-excel.ts b/src/generate-excel.ts index 6b8159e..d87e5cf 100644 --- a/src/generate-excel.ts +++ b/src/generate-excel.ts @@ -17,8 +17,8 @@ import { generateStyleXml } from "./xl/styles.xml"; import { generateTheme1 } from "./xl/theme/theme1.xml"; import { generateWorkBookXml } from "./xl/workbook.xml"; import { generateSheetXml } from "./xl/worksheets/sheet.xml"; -import { ICellType, IPage, ISheet, IWorkbook, IBorder } from "./index"; -import { Equation, SkipCell, getBorderKey } from "./util"; +import { ICellType, IPage, ISheet, IWorkbook, IBorder, ICellStyle } from "./index"; +import { Equation, SkipCell, getBorderKey, getStyleKey } from "./util"; /** * Generates the complete XML file structure for an Excel workbook @@ -28,9 +28,9 @@ import { Equation, SkipCell, getBorderKey } from "./util"; * @internal */ const generateTree = (workbook: IWorkbook) => { - // Collect all unique border styles from the workbook - const borderStyles = new Map(); - borderStyles.set("none", {}); // Default no-border style + // Collect all unique complete styles from the workbook + const styleMap = new Map(); + styleMap.set("default", {}); // Default no-style // Check if workbook contains any date cells let hasDateCells = false; @@ -43,10 +43,10 @@ const generateTree = (workbook: IWorkbook) => { hasDateCells = true; } - // Collect border styles - if ('style' in cell && cell.style?.border) { - const borderKey = getBorderKey(cell.style.border); - borderStyles.set(borderKey, cell.style.border); + // Collect complete styles (border, background, foreground colors) + if ('style' in cell && cell.style) { + const styleKey = getStyleKey(cell.style); + styleMap.set(styleKey, cell.style); } }); }); @@ -59,10 +59,10 @@ const generateTree = (workbook: IWorkbook) => { "docProps/core.xml": generateCoreXml({}), "xl/_rels/workbook.xml.rels": generateWorkBookXmlRels(workbook), "xl/sharedStrings.xml": generateSharedStrings(workbook), - "xl/styles.xml": generateStyleXml(borderStyles, hasDateCells), + "xl/styles.xml": generateStyleXml(styleMap, hasDateCells), "xl/theme/theme1.xml": generateTheme1(), "xl/workbook.xml": generateWorkBookXml(workbook), - ...workbook.sheets.reduce((acc, sheet, idx) => ({ ...acc, [`xl/worksheets/sheet${idx + 1}.xml`]: generateSheetXml(sheet, borderStyles, hasDateCells) }), {}) + ...workbook.sheets.reduce((acc, sheet, idx) => ({ ...acc, [`xl/worksheets/sheet${idx + 1}.xml`]: generateSheetXml(sheet, styleMap, hasDateCells) }), {}) }; }; diff --git a/src/index.ts b/src/index.ts index 23f923b..81079d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ */ import { generateExcel, EnvironmentType } from "./generate-excel"; -import { SkipCell, skipCell, Equation, writeEquation, createBorder, createAllBorders, createTopBorder, createBottomBorder, createLeftBorder, createRightBorder, createStyledCell, createBorderedCell, createDateCell, createBorderedDateCell } from "./util"; +import { SkipCell, skipCell, Equation, writeEquation, createBorder, createAllBorders, createTopBorder, createBottomBorder, createLeftBorder, createRightBorder, createStyledCell, createBorderedCell, createDateCell, createBorderedDateCell, createBackgroundCell, createForegroundCell, createColoredCell, createBackgroundDateCell } from "./util"; /** * Enum representing different cell types in Excel @@ -72,6 +72,10 @@ interface IBorder { interface ICellStyle { /** Border configuration for the cell */ border?: IBorder; + /** Background (fill) color in hex format (e.g., "#FFFF00" for yellow) */ + backgroundColor?: string; + /** Foreground (font) color in hex format (e.g., "#000000" for black text) */ + foregroundColor?: string; } /** @@ -273,6 +277,53 @@ const sampleData = [ 'Mixed Content' ] ] + }, + // Demonstration of color functionality + { + title: 'ColorDemo', + content: [ + [ + // Header row with colors + 'Feature', + 'Background Color', + 'Foreground Color', + 'Both Colors' + ], + [ + // Background colors + 'Yellow Background', + createBackgroundCell('Highlighted', '#FFFF00'), + createForegroundCell('Red Text', '#FF0000'), + createColoredCell('Both', '#00FF00', '#FF0000') + ], + [ + 'More Colors', + createBackgroundCell('Blue BG', '#0000FF'), + createForegroundCell('Green Text', '#00FF00'), + createColoredCell('Purple/White', '#800080', '#FFFFFF') + ], + [ + // Colored dates + 'Date Colors', + createBackgroundDateCell(new Date(), '#FFCCCC'), + createDateCell(new Date('2024-12-25'), { + backgroundColor: '#00FF00', + foregroundColor: '#FF0000' + }), + 'Mixed with dates' + ], + [ + // Combined with borders + 'With Borders', + createStyledCell('Complex', { + backgroundColor: '#FFFFCC', + foregroundColor: '#0000FF', + border: createAllBorders(BorderStyle.thick, '#000000') + }), + 'Plain text', + 42 + ] + ] } ] @@ -283,4 +334,4 @@ const sampleData = [ * This includes the main generation function, sample data, environment types, * utility functions, and all border/styling helper functions */ -export { generateExcel, sampleData, EnvironmentType, skipCell, writeEquation, createBorder, createAllBorders, createTopBorder, createBottomBorder, createLeftBorder, createRightBorder, createStyledCell, createBorderedCell, createDateCell, createBorderedDateCell }; +export { generateExcel, sampleData, EnvironmentType, skipCell, writeEquation, createBorder, createAllBorders, createTopBorder, createBottomBorder, createLeftBorder, createRightBorder, createStyledCell, createBorderedCell, createDateCell, createBorderedDateCell, createBackgroundCell, createForegroundCell, createColoredCell, createBackgroundDateCell }; diff --git a/src/util.ts b/src/util.ts index e4f6d31..24e352f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -234,6 +234,25 @@ const getBorderKey = (border?: IBorder): string => { return parts.join("-"); }; +/** + * Generates a unique key string for complete cell style configuration + * Used internally to identify unique style combinations for Excel styling + * @param {ICellStyle} style - Complete style configuration object + * @returns {string} Unique key representing the complete style configuration + * @internal + */ +const getStyleKey = (style?: ICellStyle): string => { + if (!style) return "default"; + + const parts = [ + getBorderKey(style.border), + style.backgroundColor || "no-bg", + style.foregroundColor || "no-fg" + ]; + + return parts.join("|"); +}; + /** * Creates a styled cell with custom styling options * @param {string | number} value - Cell value (string or number) @@ -320,6 +339,68 @@ const createBorderedDateCell = ( return createDateCell(date, { border }); }; +/** + * Creates a cell with background color styling + * Convenience function for applying background color to cells + * @param {string | number} value - Cell value (string or number) + * @param {string} backgroundColor - Background color in hex format (e.g., "#FFFF00") + * @returns {ICell} Cell with background color styling applied + * @example createBackgroundCell('Highlighted', '#FFFF00') + */ +const createBackgroundCell = ( + value: string | number, + backgroundColor: string +): ICell => { + return createStyledCell(value, { backgroundColor }); +}; + +/** + * Creates a cell with foreground (text) color styling + * Convenience function for applying text color to cells + * @param {string | number} value - Cell value (string or number) + * @param {string} foregroundColor - Text color in hex format (e.g., "#FF0000") + * @returns {ICell} Cell with text color styling applied + * @example createForegroundCell('Red Text', '#FF0000') + */ +const createForegroundCell = ( + value: string | number, + foregroundColor: string +): ICell => { + return createStyledCell(value, { foregroundColor }); +}; + +/** + * Creates a cell with both background and foreground color styling + * Convenience function for applying both background and text colors + * @param {string | number} value - Cell value (string or number) + * @param {string} backgroundColor - Background color in hex format + * @param {string} foregroundColor - Text color in hex format + * @returns {ICell} Cell with color styling applied + * @example createColoredCell('Styled Text', '#FFFF00', '#FF0000') + */ +const createColoredCell = ( + value: string | number, + backgroundColor: string, + foregroundColor: string +): ICell => { + return createStyledCell(value, { backgroundColor, foregroundColor }); +}; + +/** + * Creates a date cell with background color styling + * Convenience function for applying background color to date cells + * @param {Date} date - JavaScript Date object + * @param {string} backgroundColor - Background color in hex format + * @returns {ICell} Date cell with background color styling applied + * @example createBackgroundDateCell(new Date(), '#FFFF00') + */ +const createBackgroundDateCell = ( + date: Date, + backgroundColor: string +): ICell => { + return createDateCell(date, { backgroundColor }); +}; + /** * Export all utility functions and classes for external use * Includes positioning utilities, cell creation helpers, border functions, and internal utilities @@ -341,9 +422,14 @@ export { createLeftBorder, createRightBorder, getBorderKey, + getStyleKey, createStyledCell, createBorderedCell, dateToExcelSerial, createDateCell, - createBorderedDateCell + createBorderedDateCell, + createBackgroundCell, + createForegroundCell, + createColoredCell, + createBackgroundDateCell } \ No newline at end of file diff --git a/src/xl/styles.xml.ts b/src/xl/styles.xml.ts index 5b9d5ac..f919411 100644 --- a/src/xl/styles.xml.ts +++ b/src/xl/styles.xml.ts @@ -1,13 +1,13 @@ /** * @fileoverview Excel styles.xml generation - * Handles the generation of Excel styling XML including borders, fonts, and cell formats + * Handles the generation of Excel styling XML including borders, fonts, fills, and cell formats * This file creates the styles.xml file that defines all visual styling for the workbook * * @author Maifee Ul Asad * @license MIT */ -import { IBorder, BorderStyle } from ".."; +import { IBorder, BorderStyle, ICellStyle } from ".."; /** * Generates XML representation of border styling for a single border configuration @@ -62,20 +62,94 @@ const generateBorderXml = (border: IBorder): string => { `; }; +/** + * Generates XML representation of font styling for foreground color + * @param {string} color - Optional hex color string for font color + * @returns {string} XML string representing the font styling + * @internal + */ +const generateFontXml = (color?: string): string => { + const colorXml = color + ? `` + : ''; + + return ` + + + ${colorXml} + + + + `; +}; + +/** + * Generates XML representation of fill styling for background color + * @param {string} color - Optional hex color string for background color + * @returns {string} XML string representing the fill styling + * @internal + */ +const generateFillXml = (color?: string): string => { + if (!color) { + return ` + + + `; + } + + return ` + + + + + + `; +}; + /** * Generates the complete styles.xml content for an Excel workbook * Creates a comprehensive styling definition including fonts, fills, borders, and cell formats - * @param {Map} borderStyles - Map of unique border styles used in the workbook + * @param {Map} styleMap - Map of unique complete styles used in the workbook * @param {boolean} hasDateCells - Whether the workbook contains date cells requiring date formatting * @returns {string} Complete XML content for styles.xml file * @internal */ -const generateStyleXml = (borderStyles: Map, hasDateCells: boolean = false) => { - const borderArray = Array.from(borderStyles.values()); - const borderCount = borderArray.length; +const generateStyleXml = (styleMap: Map, hasDateCells: boolean = false) => { + const styles = Array.from(styleMap.values()); + const styleCount = styles.length; + + // Extract unique borders, fonts, and fills from all styles + const uniqueBorders = new Map(); + const uniqueFonts = new Map(); + const uniqueFills = new Map(); + + // Add default styles + uniqueBorders.set("none", {}); + uniqueFonts.set("default", ""); + uniqueFills.set("none", ""); + uniqueFills.set("gray125", ""); // Excel default + + // Collect unique styles + styles.forEach(style => { + if (style.border) { + uniqueBorders.set(JSON.stringify(style.border), style.border); + } + if (style.foregroundColor) { + uniqueFonts.set(style.foregroundColor, style.foregroundColor); + } + if (style.backgroundColor) { + uniqueFills.set(style.backgroundColor, style.backgroundColor); + } + }); + + // Generate XML sections + const borderArray = Array.from(uniqueBorders.values()); + const fontArray = Array.from(uniqueFonts.values()); + const fillArray = Array.from(uniqueFills.values()); - // Generate XML for all border definitions const bordersXml = borderArray.map(border => generateBorderXml(border)).join(''); + const fontsXml = fontArray.map(color => generateFontXml(color || undefined)).join(''); + const fillsXml = fillArray.map(color => generateFillXml(color || undefined)).join(''); // Generate number formats if date cells are present const numFmtsXml = hasDateCells @@ -84,28 +158,36 @@ const generateStyleXml = (borderStyles: Map, hasDateCells: bool ` : ''; - // Generate cell format definitions that reference the borders - // If we have date cells, we need both regular and date formats + // Generate cell format definitions let cellXfsXml = ''; - let cellXfsCount = borderCount; + let cellXfsCount = styleCount; if (hasDateCells) { - // Generate formats for regular cells (numFmtId=0) - cellXfsXml += borderArray.map((_, index) => - `` - ).join('\n '); + // Generate formats for regular cells + cellXfsXml += styles.map((style, index) => { + const borderIndex = Array.from(uniqueBorders.keys()).indexOf(JSON.stringify(style.border || {})); + const fontIndex = Array.from(uniqueFonts.keys()).indexOf(style.foregroundColor || "default"); + const fillIndex = Array.from(uniqueFills.keys()).indexOf(style.backgroundColor || "none"); + return ``; + }).join('\n '); - // Generate formats for date cells (numFmtId=164) + // Generate formats for date cells cellXfsXml += '\n '; - cellXfsXml += borderArray.map((_, index) => - `` - ).join('\n '); + cellXfsXml += styles.map((style, index) => { + const borderIndex = Array.from(uniqueBorders.keys()).indexOf(JSON.stringify(style.border || {})); + const fontIndex = Array.from(uniqueFonts.keys()).indexOf(style.foregroundColor || "default"); + const fillIndex = Array.from(uniqueFills.keys()).indexOf(style.backgroundColor || "none"); + return ``; + }).join('\n '); - cellXfsCount = borderCount * 2; // Double the formats for date support + cellXfsCount = styleCount * 2; } else { - cellXfsXml = borderArray.map((_, index) => - `` - ).join('\n '); + cellXfsXml = styles.map((style, index) => { + const borderIndex = Array.from(uniqueBorders.keys()).indexOf(JSON.stringify(style.border || {})); + const fontIndex = Array.from(uniqueFonts.keys()).indexOf(style.foregroundColor || "default"); + const fillIndex = Array.from(uniqueFills.keys()).indexOf(style.backgroundColor || "none"); + return ``; + }).join('\n '); } /** @@ -116,24 +198,13 @@ const generateStyleXml = (borderStyles: Map, hasDateCells: bool return ` ${numFmtsXml} - - - - - - - - + + ${fontsXml} - - - - - - - + + ${fillsXml} - + ${bordersXml} diff --git a/src/xl/worksheets/sheet.xml.ts b/src/xl/worksheets/sheet.xml.ts index e84bb8c..443ad8e 100644 --- a/src/xl/worksheets/sheet.xml.ts +++ b/src/xl/worksheets/sheet.xml.ts @@ -7,31 +7,31 @@ * @license MIT */ -import { ISheet, ICellType, IBorder } from "../.."; -import { rowColumnToVbPosition, indexToVbIndex, calculateExtant, Equation, getBorderKey, dateToExcelSerial } from '../../util' +import { ISheet, ICellType, IBorder, ICellStyle } from "../.."; +import { rowColumnToVbPosition, indexToVbIndex, calculateExtant, Equation, getBorderKey, dateToExcelSerial, getStyleKey } from '../../util' /** * Generates the complete XML content for an Excel worksheet * Creates worksheet XML with cell data, styling references, and proper Excel structure * @param {ISheet} sheet - Sheet data containing rows and cells - * @param {Map} borderStyles - Map of unique border styles used in the workbook + * @param {Map} styleMap - Map of unique complete styles used in the workbook * @param {boolean} hasDateCells - Whether the workbook contains date cells requiring special formatting * @returns {string} Complete XML content for sheet.xml file * @internal */ const generateSheetXml = ( sheet: ISheet, - borderStyles: Map, + styleMap: Map, hasDateCells: boolean = false ) => { /** - * Create a reverse mapping from border style to index - * This allows us to reference border styles by index in cell styling + * Create a reverse mapping from style to index + * This allows us to reference styles by index in cell styling * @type {Map} */ - const borderStyleToIndex = new Map(); - Array.from(borderStyles.keys()).forEach((key, index) => { - borderStyleToIndex.set(key, index); + const styleToIndex = new Map(); + Array.from(styleMap.keys()).forEach((key, index) => { + styleToIndex.set(key, index); }); /** @@ -76,26 +76,26 @@ const generateSheetXml = ( /** * Determine the style index for this cell - * Style index references the border style in styles.xml + * Style index references the complete style in styles.xml * For date cells with date formatting enabled, add offset to get date format styles - * Default to 0 (no border) if no style is specified + * Default to 0 (default style) if no style is specified */ - let styleIndex = 0; // Default to no-border style + let styleIndex = 0; // Default to no-style let isDateCell = cell.type === ICellType.date; - if ('style' in cell && cell.style?.border) { - const borderKey = getBorderKey(cell.style.border); - const baseBorderIndex = borderStyleToIndex.get(borderKey) || 0; + if ('style' in cell && cell.style) { + const styleKey = getStyleKey(cell.style); + const baseStyleIndex = styleToIndex.get(styleKey) || 0; // If this is a date cell and we have date formatting, use the date format styles if (isDateCell && hasDateCells) { - styleIndex = baseBorderIndex + borderStyles.size; // Offset for date formats + styleIndex = baseStyleIndex + styleMap.size; // Offset for date formats } else { - styleIndex = baseBorderIndex; + styleIndex = baseStyleIndex; } } else if (isDateCell && hasDateCells) { - // Date cell with no border but needs date formatting - styleIndex = borderStyles.size; // First date format style (no border) + // Date cell with no style but needs date formatting + styleIndex = styleMap.size; // First date format style (default style) } /**