@ricsam/excel-to-formula-engine 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -150,6 +150,29 @@ await excelToFormulaEngine(file, {
150
150
  | Number formats, hyperlinks | Cell metadata | Stored for your UI; the engine does not interpret them. |
151
151
  | Merges, frozen panes, column widths, hidden sheets | Sheet metadata | Same — stored for your UI to apply. |
152
152
 
153
+ ### Effective cell fonts
154
+
155
+ Existing worksheet cells resolve their formatting in this order: explicit cell
156
+ `s` (including `s="0"`), row `s` when `customFormat` is enabled, column `style`,
157
+ then workbook cell XF 0. Column ranges remain sparse, including full-sheet
158
+ ranges. Named-style font inheritance (`cellStyleXfs` / `xfId`) is resolved;
159
+ `applyFont="0"` selects the base font, while an explicit applied font replaces
160
+ it as a unit. Supported font decorations include explicit false values so a
161
+ plain font can clear inherited bold, italic, or underline.
162
+
163
+ The resolved XF 0 is also retained as `workbookMetadata.defaultCellStyle`
164
+ (engine units: font sizes are pixels). This is additive metadata for clients;
165
+ actual imported cells receive coalesced direct styles without requiring a new
166
+ engine API. `styles:false` disables both. Empty coordinates not present in the
167
+ worksheet are not materialized. Older imported snapshots do not retain XF 0
168
+ and cannot reconstruct it losslessly from neighboring styles.
169
+
170
+ Literal Excel font names are retained as `fontFamily` (clients must provide
171
+ those fonts). This is not full Excel rendering fidelity: theme font substitution,
172
+ table style themes, rich-text runs, and named-style inheritance of non-font
173
+ properties are not translated by this change. Double/accounting underline
174
+ variants become a single boolean underline.
175
+
153
176
  ### Formulas the engine cannot evaluate
154
177
 
155
178
  The engine implements around 40 functions. A formula using anything else — an
package/dist/index.cjs CHANGED
@@ -280,6 +280,11 @@ function parseWorksheet(pkg, partPath, sharedStrings) {
280
280
  function parseCells(worksheet, sharedStrings, result) {
281
281
  const sheetData = worksheet["sheetData"];
282
282
  const shared = new Map;
283
+ const columnStyles = asArray(worksheet["cols"]?.["col"]).map((col) => ({
284
+ min: numAttr(col, "min"),
285
+ max: numAttr(col, "max"),
286
+ style: numAttr(col, "style")
287
+ })).filter((col) => col.min !== undefined && col.max !== undefined && col.style !== undefined);
283
288
  for (const rawRow of asArray(sheetData?.["row"])) {
284
289
  const row = rawRow;
285
290
  const rowNumber = numAttr(row, "r");
@@ -297,11 +302,14 @@ function parseCells(worksheet, sharedStrings, result) {
297
302
  if (!address) {
298
303
  continue;
299
304
  }
300
- const styleIndex = numAttr(cell, "s");
305
+ const explicitStyle = numAttr(cell, "s");
306
+ const rowStyle = boolAttr(row, "customFormat") ? numAttr(row, "s") : undefined;
307
+ const columnStyle = columnStyles.findLast((col) => address.colIndex + 1 >= col.min && address.colIndex + 1 <= col.max)?.style;
308
+ const styleIndex = explicitStyle ?? rowStyle ?? columnStyle ?? 0;
301
309
  const type = attr(cell, "t") ?? "n";
302
310
  const formula = readFormula(cell, address, shared);
303
311
  const { kind, value } = readValue(cell, type, sharedStrings);
304
- if (kind === "empty" && formula === undefined && styleIndex === undefined) {
312
+ if (kind === "empty" && formula === undefined && explicitStyle === undefined && rowStyle === undefined && columnStyle === undefined) {
305
313
  continue;
306
314
  }
307
315
  result.cells.push({
@@ -691,14 +699,19 @@ function parseStyles(pkg) {
691
699
  const fills = asArray(styleSheet["fills"]?.["fill"]);
692
700
  const borders = asArray(styleSheet["borders"]?.["border"]);
693
701
  const cellXfs = asArray(styleSheet["cellXfs"]?.["xf"]);
702
+ const styleXfs = asArray(styleSheet["cellStyleXfs"]?.["xf"]);
694
703
  const formats = cellXfs.map((rawXf) => {
695
704
  const xf = rawXf;
696
705
  const style = {};
697
- if (boolAttr(xf, "applyFont", true)) {
698
- const font = fonts[numAttr(xf, "fontId") ?? -1];
699
- if (font) {
700
- applyFont(style, font, theme, indexedColors);
701
- }
706
+ const baseXf = styleXfs[numAttr(xf, "xfId") ?? -1];
707
+ const baseFontId = boolAttr(baseXf, "applyFont", true) ? numAttr(baseXf, "fontId") ?? 0 : 0;
708
+ const fontId = boolAttr(xf, "applyFont", true) ? numAttr(xf, "fontId") ?? baseFontId : baseFontId;
709
+ const font = fonts[fontId];
710
+ if (font) {
711
+ style.bold = false;
712
+ style.italic = false;
713
+ style.underline = false;
714
+ applyFont(style, font, theme, indexedColors);
702
715
  }
703
716
  if (boolAttr(xf, "applyFill", true)) {
704
717
  const fill = fills[numAttr(xf, "fillId") ?? -1];
@@ -743,14 +756,18 @@ function parseStyles(pkg) {
743
756
  return { formats, differentialFormats };
744
757
  }
745
758
  function applyFont(style, font, theme, indexedColors) {
746
- if (font["b"] !== undefined && isOn(font["b"])) {
747
- style.bold = true;
759
+ const fontFamily = attr(font["name"], "val");
760
+ if (fontFamily) {
761
+ style.fontFamily = fontFamily;
762
+ }
763
+ if (font["b"] !== undefined) {
764
+ style.bold = isOn(font["b"]);
748
765
  }
749
- if (font["i"] !== undefined && isOn(font["i"])) {
750
- style.italic = true;
766
+ if (font["i"] !== undefined) {
767
+ style.italic = isOn(font["i"]);
751
768
  }
752
- if (font["u"] !== undefined && isOn(font["u"])) {
753
- style.underline = true;
769
+ if (font["u"] !== undefined) {
770
+ style.underline = attr(font["u"], "val") !== "none";
754
771
  }
755
772
  const size = numAttr(font["sz"], "val");
756
773
  if (size !== undefined) {
@@ -1441,6 +1458,7 @@ async function convertWorkbook(source, options = {}) {
1441
1458
  cellStyles: cellStyles.length > 0 ? cellStyles : undefined,
1442
1459
  conditionalStyles: conditionalStyles.length > 0 ? conditionalStyles : undefined,
1443
1460
  workbookMetadata: {
1461
+ defaultCellStyle: importStyles ? styleTable.formats[0]?.style : undefined,
1444
1462
  fileName: options.fileName,
1445
1463
  sheetOrder: sheetEntries.map((entry) => entry.name),
1446
1464
  diagnostics: diagnostics.length > 0 ? diagnostics : undefined
@@ -1862,11 +1880,22 @@ async function spreadsheetToFormulaEngine(source, options) {
1862
1880
  }
1863
1881
  async function spreadsheetToFormulaEngineDetailed(source, options) {
1864
1882
  const bytes = await toBytes2(source);
1883
+ const fileName = options?.fileName ?? (typeof File !== "undefined" && source instanceof File ? source.name : undefined);
1865
1884
  if (looksLikeZip(bytes)) {
1866
1885
  return convertWorkbook(bytes, withFileName(source, options));
1867
1886
  }
1887
+ if (fileName && /\.(xlsx|xlsm)$/i.test(fileName)) {
1888
+ throw new Error(`${fileName} is named as an Excel workbook but is not one — the file is ` + `corrupt, or it is a legacy .xls saved under the wrong extension.`);
1889
+ }
1890
+ assertLooksLikeText(bytes, fileName);
1868
1891
  return csvToFormulaEngineDetailed(bytes, withCsvFileName(source, options));
1869
1892
  }
1893
+ function assertLooksLikeText(bytes, fileName) {
1894
+ const sample = bytes.subarray(0, 8192);
1895
+ if (sample.indexOf(0) !== -1) {
1896
+ throw new Error(`${fileName ?? "The file"} is not a spreadsheet: it contains binary data ` + `rather than delimited text.`);
1897
+ }
1898
+ }
1870
1899
  function looksLikeZip(bytes) {
1871
1900
  return bytes.length >= 4 && bytes[0] === 80 && bytes[1] === 75 && bytes[2] === 3 && bytes[3] === 4;
1872
1901
  }
@@ -1889,4 +1918,4 @@ function withCsvFileName(source, options) {
1889
1918
  };
1890
1919
  }
1891
1920
 
1892
- //# debugId=E62FBA8DCEBAE6FE64756E2164756E21
1921
+ //# debugId=361575F73A84A17764756E2164756E21