diff --git a/src/engine/__tests__/lexer.spec.ts b/src/engine/__tests__/lexer.spec.ts index a70d420..8ad9da2 100644 --- a/src/engine/__tests__/lexer.spec.ts +++ b/src/engine/__tests__/lexer.spec.ts @@ -62,6 +62,15 @@ describe('lexer', () => { expect(values('$A$1 A$1 $A1')).toEqual(['$A$1', 'A$1', '$A1', '']); }); + it('reads identifiers with non-ASCII letters (umlauts etc.) as a single IDENT', () => { + // a table/sheet name routinely contains "ä"/"ö"/"ü"/"ß" in this + // German-first app -- these must lex as ordinary IDENT characters, not + // throw, so an unquoted qualifier like "Übersicht::A1" actually works. + expect(types('Übersicht::B4')).toEqual(['IDENT', 'DBLCOLON', 'IDENT', 'EOF']); + expect(values('Übersicht')).toEqual(['Übersicht', '']); + expect(values('Größe_Prämie')).toEqual(['Größe_Prämie', '']); + }); + it('skips whitespace', () => { expect(types(' A1\t+\n B2 ')).toEqual(['IDENT', 'PLUS', 'IDENT', 'EOF']); }); diff --git a/src/engine/__tests__/workbook.spec.ts b/src/engine/__tests__/workbook.spec.ts index 9ad1740..44203c1 100644 --- a/src/engine/__tests__/workbook.spec.ts +++ b/src/engine/__tests__/workbook.spec.ts @@ -90,6 +90,17 @@ describe('Workbook cross-sheet', () => { expect(val(wb, 's-kosten', 'A9')).toBe(800); expect(val(wb, 's-kosten', 'B9')).toBe(800); }); + + it('a table name with non-ASCII letters works as a bare, unquoted qualifier', () => { + // regression: "Übersicht" is the fixture's own table name, but nothing + // above ever refers *into* it by name (it only ever refers out) -- so + // this table has silently been unreferenceable from elsewhere the whole + // time (the lexer rejected "Ü" outright; see engine/lexer.ts). + const wb = fixture(); + wb.setCell('s-umsatz', 6, 8, '=Übersicht!B2'); // G9, no quoting needed (no spaces) + expect(val(wb, 's-umsatz', 'G9')).toBe(5970); + expect(isError(val(wb, 's-umsatz', 'G9'))).toBe(false); + }); }); describe('Workbook error values', () => { diff --git a/src/engine/lexer.ts b/src/engine/lexer.ts index c1d51fb..11e181d 100644 --- a/src/engine/lexer.ts +++ b/src/engine/lexer.ts @@ -66,12 +66,18 @@ const SINGLE: Record = { function isDigit(ch: string): boolean { return ch >= '0' && ch <= '9'; } +/** Any Unicode letter, not just ASCII -- a table/sheet name (used unquoted + * as a formula qualifier whenever it has no spaces) routinely contains + * "ä"/"ö"/"ü"/"ß" or other accented letters in this German-first app; + * rejecting those at the lexer level made every such name effectively + * unusable as a bare qualifier (`Übersicht::A1` used to throw outright). */ +const IS_LETTER = /\p{L}/u; function isWordStart(ch: string): boolean { // '$' only ever appears inside a cell reference ("$A1", "A$1", "$A$1"), but // the lexer doesn't distinguish those from any other bare word (see the // module doc) -- it's just part of the word here, and the parser is the one // that validates/interprets it as a locked reference axis. - return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '$'; + return IS_LETTER.test(ch) || ch === '_' || ch === '$'; } function isWordPart(ch: string): boolean { return isWordStart(ch) || isDigit(ch);