@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.d.cts CHANGED
@@ -103,7 +103,7 @@ interface BuilderOptions {
103
103
  }
104
104
 
105
105
  /** This package's own version, pinned to package.json by `version.test.ts`. */
106
- declare const VERSION = "2.3.0";
106
+ declare const VERSION = "2.3.1";
107
107
  type Any$5 = any;
108
108
  /**
109
109
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -144,7 +144,7 @@ type Any$4 = any;
144
144
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
145
145
  */
146
146
  declare class HolySheet {
147
- static readonly VERSION = "2.3.0";
147
+ static readonly VERSION = "2.3.1";
148
148
  validate(schema: Any$4): ValidationError[];
149
149
  toBytes(schema: Any$4): Uint8Array;
150
150
  write(schema: Any$4, path: string): Promise<WriteResult>;
@@ -322,6 +322,12 @@ declare class Normalizer {
322
322
  declare class FormulaLinter {
323
323
  private index;
324
324
  private cache;
325
+ /**
326
+ * Every sheet name, keyed by its lower-cased form. Excel matches sheet names
327
+ * without regard to case, and a reference to a sheet that is not here is
328
+ * `#REF!` rather than a range of blanks.
329
+ */
330
+ private sheetNames;
325
331
  lint(schema: unknown): FormulaProblem[];
326
332
  private buildIndex;
327
333
  private isError;
@@ -337,6 +343,10 @@ declare class FormulaLinter {
337
343
  private parsePrimary;
338
344
  private resolveRefOrRange;
339
345
  private cleanRef;
346
+ /**
347
+ * Unwrap the single quotes Excel puts around a sheet name that needs them,
348
+ * and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
349
+ */
340
350
  private cleanSheetName;
341
351
  private resolveCell;
342
352
  private resolveRange;
@@ -353,6 +363,7 @@ declare class FormulaLinter {
353
363
  private minMax;
354
364
  private ifFn;
355
365
  private hint;
366
+ private hintRef;
356
367
  private hintValue;
357
368
  }
358
369
 
package/dist/index.d.ts CHANGED
@@ -103,7 +103,7 @@ interface BuilderOptions {
103
103
  }
104
104
 
105
105
  /** This package's own version, pinned to package.json by `version.test.ts`. */
106
- declare const VERSION = "2.3.0";
106
+ declare const VERSION = "2.3.1";
107
107
  type Any$5 = any;
108
108
  /**
109
109
  * Agent — the structured-tool surface for Holy Sheet. Mirrors PHP `Agent`.
@@ -144,7 +144,7 @@ type Any$4 = any;
144
144
  * over `Agent`. Provided for parity / DI ergonomics; most callers use `Agent`.
145
145
  */
146
146
  declare class HolySheet {
147
- static readonly VERSION = "2.3.0";
147
+ static readonly VERSION = "2.3.1";
148
148
  validate(schema: Any$4): ValidationError[];
149
149
  toBytes(schema: Any$4): Uint8Array;
150
150
  write(schema: Any$4, path: string): Promise<WriteResult>;
@@ -322,6 +322,12 @@ declare class Normalizer {
322
322
  declare class FormulaLinter {
323
323
  private index;
324
324
  private cache;
325
+ /**
326
+ * Every sheet name, keyed by its lower-cased form. Excel matches sheet names
327
+ * without regard to case, and a reference to a sheet that is not here is
328
+ * `#REF!` rather than a range of blanks.
329
+ */
330
+ private sheetNames;
325
331
  lint(schema: unknown): FormulaProblem[];
326
332
  private buildIndex;
327
333
  private isError;
@@ -337,6 +343,10 @@ declare class FormulaLinter {
337
343
  private parsePrimary;
338
344
  private resolveRefOrRange;
339
345
  private cleanRef;
346
+ /**
347
+ * Unwrap the single quotes Excel puts around a sheet name that needs them,
348
+ * and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
349
+ */
340
350
  private cleanSheetName;
341
351
  private resolveCell;
342
352
  private resolveRange;
@@ -353,6 +363,7 @@ declare class FormulaLinter {
353
363
  private minMax;
354
364
  private ifFn;
355
365
  private hint;
366
+ private hintRef;
356
367
  private hintValue;
357
368
  }
358
369
 
package/dist/index.js CHANGED
@@ -719,6 +719,7 @@ var ERR_REF = "#REF!";
719
719
  var ERR_NAME = "#NAME?";
720
720
  var ERR_DIV0 = "#DIV/0!";
721
721
  var ERR_CIRC = "#CIRC!";
722
+ var SHEET_QUALIFIER = "('(?:[^']|'')+'|[A-Za-z_][A-Za-z0-9_.]*)!";
722
723
  var LinterError = class extends Error {
723
724
  constructor(errorCode) {
724
725
  super(errorCode);
@@ -729,9 +730,16 @@ var FormulaLinter = class {
729
730
  constructor() {
730
731
  this.index = /* @__PURE__ */ new Map();
731
732
  this.cache = /* @__PURE__ */ new Map();
733
+ /**
734
+ * Every sheet name, keyed by its lower-cased form. Excel matches sheet names
735
+ * without regard to case, and a reference to a sheet that is not here is
736
+ * `#REF!` rather than a range of blanks.
737
+ */
738
+ this.sheetNames = /* @__PURE__ */ new Map();
732
739
  }
733
740
  lint(schema) {
734
741
  const workbook = new Normalizer().normalize(schema);
742
+ this.sheetNames = new Map(workbook.sheets.map((sheet) => [sheet.name.toLowerCase(), sheet.name]));
735
743
  this.index = this.buildIndex(workbook);
736
744
  this.cache = /* @__PURE__ */ new Map();
737
745
  const issues = [];
@@ -807,6 +815,24 @@ var FormulaLinter = class {
807
815
  tokens.push({ type: "STRING", value });
808
816
  continue;
809
817
  }
818
+ if (ch === "'") {
819
+ const start = i;
820
+ i++;
821
+ for (; ; ) {
822
+ if (i >= len) throw new LinterError(ERR_NAME);
823
+ if (src[i] === "'") {
824
+ if (i + 1 < len && src[i + 1] === "'") {
825
+ i += 2;
826
+ continue;
827
+ }
828
+ i++;
829
+ break;
830
+ }
831
+ i++;
832
+ }
833
+ tokens.push({ type: "SHEET", value: src.slice(start, i) });
834
+ continue;
835
+ }
810
836
  if (isAlpha(ch) || ch === "_" || ch === "$") {
811
837
  const start = i;
812
838
  while (i < len && (isAlnum(src[i]) || src[i] === "_" || src[i] === "$" || src[i] === ".")) i++;
@@ -924,16 +950,18 @@ var FormulaLinter = class {
924
950
  c.pos++;
925
951
  return tok.value;
926
952
  }
927
- if (tok.type === "IDENT") {
953
+ if (tok.type === "SHEET" || tok.type === "IDENT") {
928
954
  const next = c.tokens[c.pos + 1];
929
955
  const next2 = c.tokens[c.pos + 2];
930
956
  if (next && next.type === "OP" && next.value === "!" && next2 && next2.type === "IDENT") {
931
- const sheetName = this.cleanSheetName(tok.value);
957
+ const sheetName = this.sheetNames.get(this.cleanSheetName(tok.value).toLowerCase());
958
+ if (sheetName === void 0) throw new LinterError(ERR_REF);
932
959
  c.pos += 2;
933
960
  const startTok = c.tokens[c.pos].value;
934
961
  c.pos++;
935
962
  return this.resolveRefOrRange(startTok, sheetName, c, stack);
936
963
  }
964
+ if (tok.type === "SHEET") throw new LinterError(ERR_NAME);
937
965
  if (next && next.type === "OP" && next.value === "(") {
938
966
  const name = tok.value.toUpperCase();
939
967
  c.pos += 2;
@@ -978,9 +1006,13 @@ var FormulaLinter = class {
978
1006
  const r = ref.replace(/\$/g, "").toUpperCase();
979
1007
  return /^[A-Z]+\d+$/.test(r) ? r : null;
980
1008
  }
1009
+ /**
1010
+ * Unwrap the single quotes Excel puts around a sheet name that needs them,
1011
+ * and turn its doubled quote back into one: 'Q3 ''Final''' is Q3 'Final'.
1012
+ */
981
1013
  cleanSheetName(name) {
982
1014
  if (name.length >= 2 && name[0] === "'" && name[name.length - 1] === "'") {
983
- return name.slice(1, -1);
1015
+ return name.slice(1, -1).split("''").join("'");
984
1016
  }
985
1017
  return name;
986
1018
  }
@@ -1148,7 +1180,7 @@ var FormulaLinter = class {
1148
1180
  case ERR_VALUE:
1149
1181
  return this.hintValue(formula, sheet);
1150
1182
  case ERR_REF:
1151
- return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1183
+ return this.hintRef(formula);
1152
1184
  case ERR_NAME:
1153
1185
  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.";
1154
1186
  case ERR_DIV0:
@@ -1159,11 +1191,20 @@ var FormulaLinter = class {
1159
1191
  return "Formula evaluation failed.";
1160
1192
  }
1161
1193
  }
1194
+ hintRef(formula) {
1195
+ for (const m of formula.matchAll(new RegExp(SHEET_QUALIFIER, "g"))) {
1196
+ const name = this.cleanSheetName(m[1]);
1197
+ if (!this.sheetNames.has(name.toLowerCase())) {
1198
+ 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.`;
1199
+ }
1200
+ }
1201
+ return "A cell reference points to a cell that doesn't exist in the workbook. Check column letters and row numbers.";
1202
+ }
1162
1203
  hintValue(formula, sheet) {
1163
- const re = /(?:([A-Za-z][A-Za-z0-9_]*)!)?\$?([A-Z]+)\$?(\d+)/gi;
1204
+ const re = new RegExp(`(?:${SHEET_QUALIFIER})?\\$?([A-Z]+)\\$?(\\d+)`, "gi");
1164
1205
  const offenders = [];
1165
1206
  for (const m of formula.matchAll(re)) {
1166
- const sheetName = m[1] ? m[1] : sheet;
1207
+ const sheetName = m[1] ? this.sheetNames.get(this.cleanSheetName(m[1]).toLowerCase()) ?? this.cleanSheetName(m[1]) : sheet;
1167
1208
  const a1 = m[2].toUpperCase() + m[3];
1168
1209
  const key = sheetName + "!" + a1;
1169
1210
  const cell = this.index.get(key);
@@ -4113,7 +4154,7 @@ var holy_sheet_schema_default = {
4113
4154
  };
4114
4155
 
4115
4156
  // src/agent.ts
4116
- var VERSION = "2.3.0";
4157
+ var VERSION = "2.3.1";
4117
4158
  function toU8(input) {
4118
4159
  return input instanceof Uint8Array ? input : new Uint8Array(input);
4119
4160
  }