@trebco/treb 30.1.8 → 30.2.10

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.
Files changed (35) hide show
  1. package/dist/languages/treb-i18n-da.mjs +1 -0
  2. package/dist/languages/treb-i18n-de.mjs +1 -0
  3. package/dist/languages/treb-i18n-es.mjs +1 -0
  4. package/dist/languages/treb-i18n-fr.mjs +1 -0
  5. package/dist/languages/treb-i18n-it.mjs +1 -0
  6. package/dist/languages/treb-i18n-nl.mjs +1 -0
  7. package/dist/languages/treb-i18n-no.mjs +1 -0
  8. package/dist/languages/treb-i18n-pl.mjs +1 -0
  9. package/dist/languages/treb-i18n-pt.mjs +1 -0
  10. package/dist/languages/treb-i18n-sv.mjs +1 -0
  11. package/dist/treb-spreadsheet-light.mjs +11 -11
  12. package/dist/treb-spreadsheet.mjs +11 -11
  13. package/dist/treb.d.ts +15 -1
  14. package/esbuild-custom-element.mjs +23 -1
  15. package/esbuild-utils.mjs +1 -0
  16. package/package.json +1 -1
  17. package/treb-calculator/src/calculator.ts +41 -0
  18. package/treb-calculator/src/expression-calculator.ts +40 -32
  19. package/treb-calculator/src/functions/base-functions.ts +164 -1
  20. package/treb-calculator/src/functions/statistics-functions.ts +134 -0
  21. package/treb-calculator/src/functions/text-functions.ts +45 -0
  22. package/treb-calculator/src/utilities.ts +5 -1
  23. package/treb-data-model/src/data_model.ts +227 -2
  24. package/treb-data-model/src/index.ts +1 -0
  25. package/{treb-embed → treb-data-model}/src/language-model.ts +3 -0
  26. package/treb-data-model/src/sheet.ts +0 -13
  27. package/treb-embed/src/embedded-spreadsheet.ts +84 -25
  28. package/treb-embed/src/options.ts +18 -0
  29. package/treb-embed/src/plugin.ts +31 -0
  30. package/treb-grid/src/editors/autocomplete_matcher.ts +8 -1
  31. package/treb-grid/src/render/tile_renderer.ts +14 -0
  32. package/treb-grid/src/types/grid.ts +25 -139
  33. package/treb-parser/src/parser-types.ts +12 -0
  34. package/treb-parser/src/parser.ts +43 -2
  35. package/tsproject.json +1 -1
@@ -22,12 +22,13 @@
22
22
  import type { Sheet } from './sheet';
23
23
  import { SheetCollection } from './sheet_collection';
24
24
  import { type UnitAddress, type UnitStructuredReference, type UnitRange, Parser, QuotedSheetNameRegex, DecimalMarkType, ArgumentSeparatorType } from 'treb-parser';
25
- import type { IArea, ICellAddress, Table, CellStyle } from 'treb-base-types';
25
+ import type { IArea, ICellAddress, Table, CellStyle, CellValue } from 'treb-base-types';
26
+ import { Is2DArray } from 'treb-base-types';
26
27
  import { Area, IsCellAddress, Style } from 'treb-base-types';
27
28
  import type { SerializedNamed } from './named';
28
29
  import { NamedRangeManager } from './named';
29
30
  import type { ConnectedElementType, MacroFunction } from './types';
30
-
31
+ import type { LanguageModel } from './language-model';
31
32
 
32
33
  /**
33
34
  *
@@ -36,6 +37,9 @@ export class DataModel {
36
37
 
37
38
  public readonly parser: Parser = new Parser();
38
39
 
40
+ /** moved from embedded spreadsheet */
41
+ public language_model?: LanguageModel;
42
+
39
43
  /** document metadata */
40
44
  public document_name?: string;
41
45
 
@@ -523,5 +527,226 @@ export class DataModel {
523
527
  */
524
528
  public connected_elements: Map<number, ConnectedElementType> = new Map();
525
529
 
530
+
531
+
532
+ // --- moving translation here ----------------------------------------------
533
+
534
+
535
+ /**
536
+ * maps common language (english) -> local language. this should
537
+ * be passed in (actually set via a function).
538
+ */
539
+ private language_map?: Record<string, string>;
540
+
541
+ /**
542
+ * maps local language -> common (english). this should be constructed
543
+ * when the forward function is passed in, so there's a 1-1 correspondence.
544
+ */
545
+ private reverse_language_map?: Record<string, string>;
546
+
547
+
548
+ /**
549
+ * set the language translation map. this is a set of function names
550
+ * (in english) -> the local equivalent. both should be in canonical form,
551
+ * as that will be used when we translate one way or the other.
552
+ */
553
+ public SetLanguageMap(language_map?: Record<string, string>) {
554
+
555
+ if (!language_map) {
556
+ this.language_map = this.reverse_language_map = undefined;
557
+ }
558
+ else {
559
+
560
+ const keys = Object.keys(language_map);
561
+
562
+ // normalize forward
563
+ this.language_map = {};
564
+ for (const key of keys) {
565
+ this.language_map[key.toUpperCase()] = language_map[key];
566
+ }
567
+
568
+ // normalize backward
569
+ this.reverse_language_map = {};
570
+ for (const key of keys) {
571
+ const value = language_map[key];
572
+ this.reverse_language_map[value.toUpperCase()] = key;
573
+ }
574
+
575
+ }
576
+
577
+ /*
578
+ // we might need to update the current displayed selection. depends
579
+ // on when we expect languages to be set.
580
+
581
+ if (!this.primary_selection.empty) {
582
+ this.Select(this.primary_selection, this.primary_selection.area, this.primary_selection.target);
583
+ }
584
+ */
585
+
586
+ }
587
+
588
+ /**
589
+ * translate function from common (english) -> local language. this could
590
+ * be inlined (assuming it's only called in one place), but we are breaking
591
+ * it out so we can develop/test/manage it.
592
+ */
593
+ public TranslateFunction(value: string): string {
594
+ if (this.language_map) {
595
+ return this.TranslateInternal(value, this.language_map, this.language_model?.boolean_true, this.language_model?.boolean_false);
596
+ }
597
+ return value;
598
+ }
599
+
600
+ /**
601
+ * translate from local language -> common (english).
602
+ * @see TranslateFunction
603
+ */
604
+ public UntranslateFunction(value: string): string {
605
+ if (this.reverse_language_map) {
606
+ return this.TranslateInternal(value, this.reverse_language_map, 'TRUE', 'FALSE');
607
+ }
608
+ return value;
609
+ }
610
+
611
+ public UntranslateData(value: CellValue|CellValue[]|CellValue[][]): CellValue|CellValue[]|CellValue[][] {
612
+
613
+ if (Array.isArray(value)) {
614
+
615
+ // could be 1d or 2d. typescript is complaining, not sure why...
616
+
617
+ if (Is2DArray(value)) {
618
+ return value.map(row => row.map(entry => {
619
+ if (entry && typeof entry === 'string' && entry[0] === '=') {
620
+ return this.UntranslateFunction(entry);
621
+ }
622
+ return entry;
623
+ }));
624
+ }
625
+ else {
626
+ return value.map(entry => {
627
+ if (entry && typeof entry === 'string' && entry[0] === '=') {
628
+ return this.UntranslateFunction(entry);
629
+ }
630
+ return entry;
631
+ });
632
+ }
633
+
634
+ }
635
+ else if (value && typeof value === 'string' && value[0] === '=') {
636
+
637
+ // single value
638
+ value = this.UntranslateFunction(value);
639
+
640
+ }
641
+
642
+ return value;
643
+
644
+ }
645
+
646
+
647
+ /**
648
+ * translation back and forth is the same operation, with a different
649
+ * (inverted) map. although it still might be worth inlining depending
650
+ * on cost.
651
+ *
652
+ * FIXME: it's about time we started using proper maps, we dropped
653
+ * support for IE11 some time ago.
654
+ */
655
+ private TranslateInternal(value: string, map: Record<string, string>, boolean_true?: string, boolean_false?: string): string {
656
+
657
+ const parse_result = this.parser.Parse(value);
658
+
659
+ if (parse_result.expression) {
660
+
661
+ let modified = false;
662
+ this.parser.Walk(parse_result.expression, unit => {
663
+ if (unit.type === 'call') {
664
+ const replacement = map[unit.name.toUpperCase()];
665
+ if (replacement) {
666
+ modified = true;
667
+ unit.name = replacement;
668
+ }
669
+ }
670
+ else if (unit.type === 'literal' && typeof unit.value === 'boolean') {
671
+
672
+ // to/from english/locale depends on the direction, but we're not
673
+ // passing that in? FIXME, pass it as a parameter. it doesn't matter
674
+ // here, but later when we render.
675
+
676
+ modified = true;
677
+
678
+ }
679
+
680
+ return true;
681
+ });
682
+
683
+ if (modified) {
684
+ return '=' + this.parser.Render(parse_result.expression, {
685
+ missing: '', boolean_true, boolean_false,
686
+ });
687
+ }
688
+ }
689
+
690
+ return value;
691
+
692
+ }
693
+
694
+
695
+ /**
696
+ * this is not public _yet_
697
+ *
698
+ * @internal
699
+ */
700
+ public SetLanguage(model?: LanguageModel): void {
701
+
702
+ this.language_model = model;
703
+
704
+ if (!model) {
705
+ this.SetLanguageMap(); // clear
706
+
707
+ // set defaults for parsing.
708
+
709
+ this.parser.flags.boolean_true = 'TRUE';
710
+ this.parser.flags.boolean_false = 'FALSE';
711
+
712
+ }
713
+ else {
714
+
715
+ // create a name map for grid
716
+
717
+ const map: Record< string, string > = {};
718
+
719
+ if (model.functions) {
720
+ for (const entry of model.functions || []) {
721
+ map[entry.base.toUpperCase()] = entry.name; // toUpperCase because of a data error -- fix at the source
722
+ }
723
+ }
724
+
725
+ this.SetLanguageMap(map);
726
+
727
+ // console.info({map});
728
+
729
+ if (!model.boolean_false) {
730
+ model.boolean_false = map['FALSE'];
731
+ }
732
+ if (!model.boolean_true) {
733
+ model.boolean_true = map['TRUE'];
734
+ }
735
+
736
+ // set defaults for parsing.
737
+
738
+ this.parser.flags.boolean_true = model.boolean_true || 'TRUE';
739
+ this.parser.flags.boolean_false = model.boolean_false || 'FALSE';
740
+
741
+ // console.info("booleans:", this.model.parser.flags.boolean_true, ",", this.model.parser.flags.boolean_false)
742
+
743
+ }
744
+
745
+ for (const sheet of this.sheets.list) {
746
+ sheet.FlushCellStyles();
747
+ }
748
+
749
+ }
750
+
526
751
  }
527
752
 
@@ -34,4 +34,5 @@ export type { AnnotationData, AnnotationType } from './annotation';
34
34
 
35
35
  export * from './data-validation';
36
36
  export * from './types';
37
+ export * from './language-model';
37
38
 
@@ -37,5 +37,8 @@ export interface LanguageModel {
37
37
  version?: string;
38
38
  locale?: string;
39
39
  functions?: TranslatedFunctionDescriptor[];
40
+
41
+ boolean_true?: string;
42
+ boolean_false?: string;
40
43
  }
41
44
 
@@ -807,19 +807,6 @@ export class Sheet {
807
807
  }
808
808
  }
809
809
 
810
- /* *
811
- * factory method creates a sheet from a 2D array.
812
- *
813
- * /
814
- public static FromArray(data: any[] = [], transpose = false): Sheet {
815
- const sheet = new Sheet();
816
- sheet.cells.FromArray(data, transpose);
817
-
818
- return sheet;
819
- }
820
- */
821
-
822
-
823
810
  // --- public methods -------------------------------------------------------
824
811
 
825
812
  public MergeCells(area: Area): void {
@@ -49,7 +49,10 @@ import type {
49
49
  CondifionalFormatExpressionOptions,
50
50
  ConditionalFormatCellMatchOptions,
51
51
  ConditionalFormatCellMatch,
52
-
52
+
53
+ LanguageModel,
54
+ TranslatedFunctionDescriptor,
55
+
53
56
  } from 'treb-data-model';
54
57
 
55
58
 
@@ -88,7 +91,6 @@ import { Spinner } from './spinner';
88
91
  import { type EmbeddedSpreadsheetOptions, DefaultOptions, type ExportOptions } from './options';
89
92
  import { type TREBDocument, SaveFileType, LoadSource, type EmbeddedSheetEvent, type InsertTableOptions } from './types';
90
93
 
91
- import type { LanguageModel, TranslatedFunctionDescriptor } from './language-model';
92
94
  import type { SelectionState } from './selection-state';
93
95
  import type { BorderToolbarMessage, ToolbarMessage } from './toolbar-message';
94
96
 
@@ -170,7 +172,6 @@ export interface LoadDocumentOptions {
170
172
  source?: LoadSource,
171
173
  }
172
174
 
173
-
174
175
  /**
175
176
  * options for the GetRange method
176
177
  */
@@ -255,6 +256,15 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
255
256
  /** @internal */
256
257
  public static treb_embedded_script_path = '';
257
258
 
259
+ /**
260
+ * @internal
261
+ *
262
+ * keep track of modules we've tried to load dynamically, so we don't
263
+ * do it again. not sure if this is strictly necessary but especially if
264
+ * we're logging I don't want to see it again
265
+ */
266
+ protected static failed_dynamic_modules: string[] = [];
267
+
258
268
  /* * @internal */
259
269
  // public static enable_engine = false;
260
270
 
@@ -333,8 +343,9 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
333
343
  return Localization;
334
344
  }
335
345
 
336
- /** loaded language model, if any */
346
+ /* * loaded language model, if any (moved to data model) * /
337
347
  protected language_model?: LanguageModel;
348
+ */
338
349
 
339
350
  /** FIXME: fix type (needs to be extensible) */
340
351
  protected events = new EventSource<{ type: string }>();
@@ -646,8 +657,6 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
646
657
  */
647
658
  constructor(options: EmbeddedSpreadsheetOptions & { model?: EmbeddedSpreadsheet }) {
648
659
 
649
- // super();
650
-
651
660
  // we renamed this option, default to the new name
652
661
 
653
662
  if (options.storage_key && !options.local_storage) {
@@ -842,6 +851,24 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
842
851
  this.grid.headless = true; // FIXME: move into grid options
843
852
  }
844
853
 
854
+ // --- testing dynamic loading ---------------------------------------------
855
+
856
+ this.LoadLanguage(options.language);
857
+
858
+ // --- testing plugins -----------------------------------------------------
859
+
860
+ /*
861
+ // FIXME: when to do this? could it wait? should it be async? (...)
862
+
863
+ if (options.plugins) {
864
+ for (const plugin of options.plugins) {
865
+ plugin.Attach(this);
866
+ }
867
+ }
868
+ */
869
+
870
+ // -------------------------------------------------------------------------
871
+
845
872
  // we're now gating this on container to support fully headless operation
846
873
 
847
874
  if (container) {
@@ -1180,15 +1207,28 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1180
1207
 
1181
1208
  let list: FunctionDescriptor[] = this.calculator.SupportedFunctions();
1182
1209
 
1183
- if (this.language_model) {
1210
+ if (this.model.language_model?.functions) {
1184
1211
 
1185
1212
  const map: Record<string, TranslatedFunctionDescriptor> = {};
1186
- for (const entry of this.language_model.functions || []) {
1213
+ for (const entry of this.model.language_model.functions || []) {
1187
1214
  map[entry.base.toUpperCase()] = entry;
1188
1215
  }
1189
1216
 
1190
1217
  list = list.map(descriptor => {
1191
- return map[descriptor.name.toUpperCase()] || descriptor;
1218
+ const partial = map[descriptor.name.toUpperCase()];
1219
+
1220
+ // FIXME: this is not deep enough if we are going to modify
1221
+ // argument names. we will need to keep other elements of the
1222
+ // argument entries. this is sufficient for now if we're just
1223
+ // setting function names / descriptions.
1224
+
1225
+ if (partial) {
1226
+ return {
1227
+ ...descriptor,
1228
+ ...partial,
1229
+ };
1230
+ }
1231
+ return descriptor;
1192
1232
  });
1193
1233
 
1194
1234
  }
@@ -1956,29 +1996,48 @@ export class EmbeddedSpreadsheet<USER_DATA_TYPE = unknown> {
1956
1996
 
1957
1997
  // --- public API methods ----------------------------------------------------
1958
1998
 
1959
- /**
1960
- * this is not public _yet_
1961
- *
1962
- * @internal
1963
- */
1964
- public SetLanguage(model?: LanguageModel): void {
1999
+ /** dynamically load language module */
2000
+ public async LoadLanguage(language = '') {
1965
2001
 
1966
- this.language_model = model;
1967
-
1968
- if (!model) {
1969
- this.grid.SetLanguageMap(); // clear
2002
+ if (!language || language === 'locale') {
2003
+ const locale = Localization.locale || '';
2004
+ const parts = locale.split(/-/).map(part => part.toLowerCase());
2005
+ language = parts[0];
1970
2006
  }
1971
- else {
1972
2007
 
1973
- // create a name map for grid
2008
+ language = language.toLowerCase();
2009
+
2010
+ let mod: { LanguageMap: LanguageModel } | undefined;
2011
+
2012
+ if (language && language !== 'en') {
2013
+
2014
+ // FIXME: even though we now have a dynamic import
2015
+ // working, we still probably want to use a filter
2016
+ // list to avoid unnecessary 404s.
1974
2017
 
1975
- const map: Record< string, string > = {};
1976
- for (const entry of model.functions || []) {
1977
- map[entry.base] = entry.name;
2018
+ // regarding the import, this is specially crafted to
2019
+ // work in both esbuild and vite. probably will break
2020
+ // some other bundlers though -- might need some magic
2021
+ // comments
2022
+
2023
+ try {
2024
+ mod = await import(`esbuild-ignore-import:./languages/treb-i18n-${language}.mjs`);
2025
+ }
2026
+ catch (err) {
2027
+ console.error(err);
1978
2028
  }
1979
- this.grid.SetLanguageMap(map);
2029
+
1980
2030
  }
1981
2031
 
2032
+ if (mod) {
2033
+ this.model.SetLanguage(mod.LanguageMap);
2034
+ }
2035
+ else {
2036
+ this.model.SetLanguage();
2037
+ }
2038
+
2039
+ this.grid.Reselect();
2040
+ this.grid.Update(true);
1982
2041
  this.UpdateAC();
1983
2042
 
1984
2043
  }
@@ -22,6 +22,7 @@
22
22
  import type { ICellAddress } from 'treb-base-types';
23
23
  import type { TREBDocument } from './types';
24
24
  import type { ChartRenderer } from 'treb-charts';
25
+ // import type { TREBPlugin } from './plugin';
25
26
 
26
27
  /**
27
28
  * factory type for chart renderer, if you want instances (pass a constructor)
@@ -327,6 +328,23 @@ export interface EmbeddedSpreadsheetOptions {
327
328
  */
328
329
  spill?: boolean;
329
330
 
331
+ /**
332
+ * language. at the moment this controls spreadsheet function names
333
+ * only; the plan is to expand to the rest of the interface over time.
334
+ * should be an ISO 639-1 language code, like "en", "fr" or "sv" (case
335
+ * insensitive). we only support a limited subset of languages at the
336
+ * moment.
337
+ *
338
+ * leave blank or set to "locale" to use the current locale.
339
+ */
340
+ language?: string;
341
+
342
+ /* *
343
+ * @internal
344
+ * testing plugins
345
+ */
346
+ // plugins?: TREBPlugin[];
347
+
330
348
  }
331
349
 
332
350
  /**
@@ -0,0 +1,31 @@
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2024 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ import type { EmbeddedSpreadsheet } from './embedded-spreadsheet';
23
+
24
+ /**
25
+ * @internal
26
+ *
27
+ * testing plugins
28
+ */
29
+ export interface TREBPlugin {
30
+ Attach: (instance: EmbeddedSpreadsheet) => void;
31
+ }
@@ -151,6 +151,8 @@ export class AutocompleteMatcher {
151
151
  return {};
152
152
  }
153
153
 
154
+ // console.info(data);
155
+
154
156
  let match;
155
157
  let result: AutocompleteExecResult = {};
156
158
 
@@ -162,7 +164,8 @@ export class AutocompleteMatcher {
162
164
  // if it's a token, and ends with a legal character
163
165
  // UPDATE: adding the negative leading \d to fix entering complex numbers
164
166
 
165
- match = data.text.match(/(?:^|[^A-Za-z_\d])([A-Za-z_][\w\d_.]*)\s*$/);
167
+ // match = data.text.match(/(?:^|[^A-Za-z_\d])([A-Za-z_][\w\d_.]*)\s*$/);
168
+ match = data.text.match(/(?:^|[^a-zA-Z\u00C0-\u024F_\d])([a-zA-Z\u00C0-\u024F_][\w\d\u00C0-\u024F_.]*)\s*$/);
166
169
 
167
170
  if (match) {
168
171
  const token = match[1];
@@ -178,6 +181,9 @@ export class AutocompleteMatcher {
178
181
  };
179
182
 
180
183
  }
184
+ else {
185
+ // console.info("NOP");
186
+ }
181
187
 
182
188
  }
183
189
 
@@ -261,6 +267,7 @@ export class AutocompleteMatcher {
261
267
  if ( (char >= 0x61 && char <= 0x7a) // a-z
262
268
  || (char >= 0x41 && char <= 0x5a) // A-Z
263
269
  || (char >= 0x30 && char <= 0x39) // 0-9
270
+ || (char >= 0x00C0 && char <= 0x024F) // accented characters
264
271
  || (char === 0x5f) // _
265
272
  || (char === 0x2e)) { // .
266
273
 
@@ -727,6 +727,20 @@ export class TileRenderer {
727
727
  let pad_entry: RenderTextPart | undefined;
728
728
  let composite_width = 0;
729
729
 
730
+ // -------------------------------------------------------------------------
731
+
732
+ // moved translated boolean formatting here. I don't like this. it
733
+ // should be in sheet (at least that's where the rest of formatting
734
+ // is), but sheet doesn't have a reference to data model (and because
735
+ // sheets are owned by data model, I don't want to add a circular ref).
736
+
737
+ if (cell.rendered_type === ValueType.boolean) {
738
+ const value = cell.calculated_type ? cell.calculated : cell.value;
739
+ cell.formatted = value ? (this.model.language_model?.boolean_true || 'TRUE') : (this.model.language_model?.boolean_false || 'FALSE');
740
+ }
741
+
742
+ // -------------------------------------------------------------------------
743
+
730
744
  let override_formatting: string | undefined;
731
745
  let formatted = cell.editing ? '' : cell.formatted; // <-- empty on editing, to remove overflows
732
746