@particle-academy/holy-sheet 2.3.0 → 2.3.1

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/dist/index.cjs CHANGED
@@ -721,6 +721,7 @@ var ERR_REF = "#REF!";
721
721
  var ERR_NAME = "#NAME?";
722
722
  var ERR_DIV0 = "#DIV/0!";
723
723
  var ERR_CIRC = "#CIRC!";
724
+ var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
724
725
  var LinterError = class extends Error {
725
726
  constructor(errorCode) {
726
727
  super(errorCode);
@@ -731,9 +732,16 @@ var FormulaLinter = class {
731
732
  constructor() {
732
733
  this.index = /* @__PURE__ */ new Map();
733
734
  this.cache = /* @__PURE__ */ new Map();
735
+ /**
736
+ * Every sheet name, keyed by its lower-cased form. Excel matches sheet names
737
+ * without regard to case, and a reference to a sheet that is not here is
738
+ * `#REF!` rather than a range of blanks.
739
+ */
740
+ this.sheetNames = /* @__PURE__ */ new Map();
734
741
  }
735
742
  lint(schema) {
736
743
  const workbook = new Normalizer().normalize(schema);
744
+ this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
737
745
  this.index = this.buildIndex(workbook);
738
746
  this.cache = /* @__PURE__ */ new Map();
739
747
  const issues = [];
@@ -809,6 +817,24 @@ var FormulaLinter = class {
809
817
  tokens.push({ type: "STRING", value });
810
818
  continue;
811
819
  }
820
+ if (ch === "'") {
821
+ const start = i;
822
+ i++;
823
+ for (; ; ) {
824
+ if (i >= len) throw new LinterError(ERR_NAME);
825
+ if (src[i] === "'") {
826
+ if (i + 1 < len && src[i + 1] === "'") {
827
+ i += 2;
828
+ continue;
829
+ }
830
+ i++;
831
+ break;
832
+ }
833
+ i++;
834
+ }
835
+ tokens.push({ type: "SHEET", value: src.slice(start, i) });
836
+ continue;
837
+ }
812
838
  if (isAlpha(ch) || ch === "_" || ch === "$") {
813
839
  const start = i;
814
840
  while (i < len && (isAlnum(src[i]) || src[i] === "_" || src[i] === "$" || src[i] === ".")) i++;
@@ -926,16 +952,18 @@ var FormulaLinter = class {
926
952
  c.pos++;
927
953
  return tok.value;
928
954
  }
929
- if (tok.type === "IDENT") {
955
+ if (tok.type === "SHEET" || tok.type === "IDENT") {
930
956
  const next = c.tokens[c.pos + 1];
931
957
  const next2 = c.tokens[c.pos + 2];
932
958
  if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
933
- const sheetName = this.cleanSheetName(tok.value);
959
+ const sheetName = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
960
+ if (sheetName === void 0) throw new LinterError(ERR_REF);
934
961
  c.pos += 2;
935
962
  const startTok = c.tokens[c.pos].value;
936
963
  c.pos++;
937
964
  return this.resolveRefOrRange(startTok, sheetName, c, stack);
938
965
  }
966
+ if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
939
967
  if (next && next.type === "OP" && next.value === "(") {
940
968
  const name = tok.value.toUpperCase();
941
969
  c.pos += 2;
@@ -980,9 +1008,13 @@ var FormulaLinter = class {
980
1008
  const r = ref.replace(/\$/g, "").toUpperCase();
981
1009
  return /^[A-Z]+\d+$/.test(r) ? r : null;
982
1010
  }
1011
+ /**
1012
+ * Unwrap the single quotes Excel puts around a sheet name that needs them,
1013
+ * and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
1014
+ */
983
1015
  cleanSheetName(name) {
984
1016
  if (name.length >= 2 && name[0] === "'" && name[name.length - 1] === "'") {
985
- return name.slice(1, -1);
1017
+ return name.slice(1, -1).split("''").join("'");
986
1018
  }
987
1019
  return name;
988
1020
  }
@@ -1150,7 +1182,7 @@ var FormulaLinter = class {
1150
1182
  case ERR_VALUE:
1151
1183
  return this.hintValue(formula, sheet);
1152
1184
  case ERR_REF:
1153
- return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1185
+ return this.hintRef(formula);
1154
1186
  case ERR_NAME:
1155
1187
  return "The formula references an unknown function or has a syntax error. Holy Sheet supports: SUM, AVERAGE, COUNT, COUNTA, MIN, MAX, IF, ROUND, ABS, LEN, UPPER, LOWER, CONCAT.";
1156
1188
  case ERR_DIV0:
@@ -1161,11 +1193,20 @@ var FormulaLinter = class {
1161
1193
  return "Formula evaluation failed.";
1162
1194
  }
1163
1195
  }
1196
+ hintRef(formula) {
1197
+ for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1198
+ const name = this.cleanSheetName(m[1]);
1199
+ if (!this.sheetNames.has(name.toLowerCase())) {
1200
+ return `The formula refers to a sheet named '${name}', and this workbook has no such sheet. Its sheets are: ${[...this.sheetNames.values()].join(", ")}. Quote a name that contains spaces or punctuation: 'My Sheet'!A1.`;
1201
+ }
1202
+ }
1203
+ return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1204
+ }
1164
1205
  hintValue(formula, sheet) {
1165
- const re = /(?:([A-Za-z][A-Za-z0-9_]*)!)?\$?([A-Z]+)\$?(\d+)/gi;
1206
+ const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
1166
1207
  const offenders = [];
1167
1208
  for (const m of formula.matchAll(re)) {
1168
- const sheetName = m[1] ? m[1] : sheet;
1209
+ const sheetName = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1169
1210
  const a1 = m[2].toUpperCase() + m[3];
1170
1211
  const key = sheetName + "!" + a1;
1171
1212
  const cell = this.index.get(key);
@@ -4115,7 +4156,7 @@ var holy_sheet_schema_default = {
4115
4156
  };
4116
4157
 
4117
4158
  // src/agent.ts
4118
- var VERSION = "2.3.0";
4159
+ var VERSION = "2.3.1";
4119
4160
  function toU8(input) {
4120
4161
  return input instanceof Uint8Array ? input : new Uint8Array(input);
4121
4162
  }