@ricsam/excel-to-formula-engine 0.2.1 → 0.3.0

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) {
@@ -1110,6 +1127,7 @@ function contractThisRowReferences(formulaBody, containingTableName) {
1110
1127
  var import_formula_engine2 = require("@ricsam/formula-engine");
1111
1128
  function translateConditionalFormat(rule, options) {
1112
1129
  const diagnostics = [];
1130
+ const ranges = rule.ranges.map((reference) => reference.replace(/\$/g, ""));
1113
1131
  const areas = [];
1114
1132
  for (const reference of rule.ranges) {
1115
1133
  const range = rangeReferenceToSpreadsheetRange(reference);
@@ -1121,14 +1139,16 @@ function translateConditionalFormat(rule, options) {
1121
1139
  return { diagnostics };
1122
1140
  }
1123
1141
  const anchor = rule.ranges[0]?.split(":")[0]?.replace(/\$/g, "") ?? "A1";
1124
- if (rule.type === "colorScale") {
1125
- return translateColorScale(rule, areas, options, diagnostics);
1126
- }
1127
- const condition = buildFormulaCondition(rule, anchor, options, diagnostics);
1128
- if (!condition) {
1129
- return { diagnostics };
1142
+ const result = rule.type === "colorScale" ? translateColorScale(rule, areas, options, diagnostics).result : withCondition(areas, buildFormulaCondition(rule, anchor, options, diagnostics));
1143
+ for (const diagnostic of diagnostics) {
1144
+ if (diagnostic.ranges === undefined && ranges.length > 0) {
1145
+ diagnostic.ranges = ranges;
1146
+ }
1130
1147
  }
1131
- return { result: { areas, condition }, diagnostics };
1148
+ return { result, diagnostics };
1149
+ }
1150
+ function withCondition(areas, condition) {
1151
+ return condition ? { areas, condition } : undefined;
1132
1152
  }
1133
1153
  function buildFormulaCondition(rule, anchor, options, diagnostics) {
1134
1154
  const color = ruleColor(rule, options);
@@ -1441,6 +1461,7 @@ async function convertWorkbook(source, options = {}) {
1441
1461
  cellStyles: cellStyles.length > 0 ? cellStyles : undefined,
1442
1462
  conditionalStyles: conditionalStyles.length > 0 ? conditionalStyles : undefined,
1443
1463
  workbookMetadata: {
1464
+ defaultCellStyle: importStyles ? styleTable.formats[0]?.style : undefined,
1444
1465
  fileName: options.fileName,
1445
1466
  sheetOrder: sheetEntries.map((entry) => entry.name),
1446
1467
  diagnostics: diagnostics.length > 0 ? diagnostics : undefined
@@ -1900,4 +1921,4 @@ function withCsvFileName(source, options) {
1900
1921
  };
1901
1922
  }
1902
1923
 
1903
- //# debugId=5AA8F8A121F02B2D64756E2164756E21
1924
+ //# debugId=F34603A2925533A764756E2164756E21