import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js'; import {serializeToString, createDocument} from './DOMUtils.js' /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ const stylesXml = ` `; const manifestXml = ` `; /** * Crée un fichier .ods à partir d'un Map de feuilles de calcul * @param {Map} sheetsData * @returns {Promise} */ export async function createOdsFile(sheetsData, currencyData = null) { // Create a new zip writer const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet')); // The “mimetype” file shall be the first file of the zip file. // It shall not be compressed, and it shall not use an 'extra field' in its header. // https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part2-packages/OpenDocument-v1.3-os-part2-packages.html#__RefHeading__752809_826425813 zipWriter.add( "mimetype", new TextReader("application/vnd.oasis.opendocument.spreadsheet"), { compressionMethod: 0, level: 0, dataDescriptor: false, extendedTimestamp: false, } ); const contentXml = generateContentFileXMLString(sheetsData, currencyData); zipWriter.add("content.xml", new TextReader(contentXml), {level: 9}); zipWriter.add("styles.xml", new TextReader(stylesXml)); zipWriter.add('META-INF/manifest.xml', new TextReader(manifestXml)); // Close the zip writer and get the ArrayBuffer const zipFile = await zipWriter.close(); return zipFile.arrayBuffer(); } /** * Generate the content.xml file with spreadsheet data * @param {Map} sheetsData * @returns {string} */ function generateContentFileXMLString(sheetsData, currencyData) { const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content'); const root = doc.documentElement; // Set up namespaces root.setAttribute('xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'); root.setAttribute('xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'); root.setAttribute('xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'); root.setAttribute('xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'); root.setAttribute('xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'); root.setAttribute('office:version', '1.2'); const styleNode = doc.createElement("office:automatic-styles"); var currencyStyleName = "currencyStyle"; if (currencyData != null) { currencyStyleName = `currency${currencyData.currencyCode.toUpperCase()}`; const numberStyle = doc.createElement("number:currency-style"); numberStyle.setAttribute("style:name", currencyStyleName); const numberCurrencySymbolStyle = doc.createElement("number:currency-symbol"); numberCurrencySymbolStyle.setAttribute("number:language", "en"); numberCurrencySymbolStyle.setAttribute("number:country", currencyData.countryCode.toUpperCase()); numberCurrencySymbolStyle.textContent = currencyData.currencySymbol; numberStyle.appendChild(numberCurrencySymbolStyle); const numberCurrencyStyle = doc.createElement("number:number"); numberCurrencyStyle.setAttribute("number:min-integer-digits", "1"); numberCurrencyStyle.setAttribute("number:decimal-places", `${currencyData.decimalPlaces}`); numberCurrencyStyle.setAttribute("number:min-decimal-places", `${currencyData.decimalPlaces}`); numberCurrencyStyle.setAttribute("number:grouping", "true"); numberStyle.appendChild(numberCurrencyStyle); styleNode.appendChild(numberStyle); const currencyCellStyleNode = doc.createElement("style:style"); currencyCellStyleNode.setAttribute("style:name", "currencycell"); currencyCellStyleNode.setAttribute("style:family", "table-cell"); currencyCellStyleNode.setAttribute("style:data-style-name", currencyStyleName); const currencyCellTableCellProperties = doc.createElement("style:table-cell-properties"); currencyCellStyleNode.appendChild(currencyCellTableCellProperties); styleNode.appendChild(currencyCellStyleNode); } const boldCellStyleNode = doc.createElement("style:style"); boldCellStyleNode.setAttribute("style:name", "boldcell"); boldCellStyleNode.setAttribute("style:family", "table-cell"); const boldCellTextPropsNode = doc.createElement("style:text-properties"); boldCellTextPropsNode.setAttribute("fo:font-weight", "bold"); boldCellStyleNode.appendChild(boldCellTextPropsNode); styleNode.appendChild(boldCellStyleNode); root.appendChild(styleNode); const bodyNode = doc.createElement('office:body'); root.appendChild(bodyNode); const spreadsheetNode = doc.createElement('office:spreadsheet'); bodyNode.appendChild(spreadsheetNode); // Iterate through sheets sheetsData.forEach((sheetData, sheetName) => { const tableNode = doc.createElement('table:table'); tableNode.setAttribute('table:name', sheetName); spreadsheetNode.appendChild(tableNode); var columnsWidthChars = {}; for (let r = 0; r < sheetData.length; r++) { for (let c = 0; c < sheetData[r].length; c++) { var len = ((sheetData[r][c].display ?? sheetData[r][c].value) + "").length; if (typeof columnsWidthChars[c] == "undefined") { columnsWidthChars[c] = len; } columnsWidthChars[c] = Math.max(columnsWidthChars[c], len); } } for (var prop in columnsWidthChars) { var columnNode = doc.createElement('table:table-column'); columnNode.setAttribute("table:style-name", "colwidth" + columnsWidthChars[prop]); tableNode.appendChild(columnNode); var columnWidthNode = doc.createElement("style:style"); columnWidthNode.setAttribute("style:name", "colwidth" + columnsWidthChars[prop]); columnWidthNode.setAttribute("style:family", "table-column"); const columnWidthPropsNode = doc.createElement("style:table-column-properties"); columnWidthPropsNode.setAttribute("style:column-width", `${columnsWidthChars[prop] * 0.26}cm`); columnWidthNode.appendChild(columnWidthPropsNode); styleNode.appendChild(columnWidthNode); } // Iterate through rows sheetData.forEach((row) => { const rowNode = doc.createElement('table:table-row'); tableNode.appendChild(rowNode); // Iterate through cells in row row.forEach((cell) => { const cellNode = doc.createElement('table:table-cell'); const cellType = convertCellType(cell.type); cellNode.setAttribute('office:value-type', cellType); if (cell.style && cell.style == "bold") { cellNode.setAttribute('table:style-name', "boldcell"); } // Add value attribute based on type if (cell.value !== null && cell.value !== undefined) { switch (cellType) { case 'float': cellNode.setAttribute('office:value', cell.value.toString()); break; case 'percentage': cellNode.setAttribute('office:value', cell.value.toString()); cellNode.setAttribute('office:value-type', 'percentage'); break; case 'currency': cellNode.setAttribute('office:value', cell.value.toString()); cellNode.setAttribute('office:value-type', 'currency'); if (currencyData != null) { cellNode.setAttribute("table:style-name", "currencycell"); cellNode.setAttribute('office:currency', currencyData.currencyCode.toUpperCase()); } break; case 'date': cellNode.setAttribute('office:date-value', cell.value.toString()); break; case 'boolean': cellNode.setAttribute('office:boolean-value', cell.value ? 'true' : 'false'); break; default: const textNode = doc.createElement('text:p'); textNode.textContent = cell.value.toString(); cellNode.appendChild(textNode); break; } if (cellType !== 'string') { const textNode = doc.createElement('text:p'); if (typeof cell.display != "undefined") { textNode.textContent = cell.display.toString(); } else { textNode.textContent = cell.value.toString(); } cellNode.appendChild(textNode); } } rowNode.appendChild(cellNode); }); }); }); return serializeToString(doc); } /** * Convert cell type to OpenDocument format type * @param {SheetCellRawContent['type']} type * @returns {SheetCellRawContent['type']} */ function convertCellType(type) { const typeMap = { 'float': 'float', 'percentage': 'percentage', 'currency': 'currency', 'date': 'date', 'time': 'time', 'boolean': 'boolean', 'string': 'string', 'n': 'float', 's': 'string', 'd': 'date', 'b': 'boolean' }; return typeMap[type] || 'string'; }