@trebco/treb 28.15.0 → 28.17.4

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/treb.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /*! API v28.15. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
1
+ /*! API v28.17. Copyright 2018-2024 trebco, llc. All rights reserved. LGPL: https://treb.app/license */
2
2
 
3
3
  /**
4
4
  * add our tag to the map
@@ -271,6 +271,11 @@ export interface EmbeddedSpreadsheetOptions {
271
271
  * change in the future. key modifiers have no effect.
272
272
  */
273
273
  recalculate_on_f9?: boolean;
274
+
275
+ /**
276
+ * indent/outdent buttons; default false
277
+ */
278
+ indent_buttons?: boolean;
274
279
  }
275
280
 
276
281
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trebco/treb",
3
- "version": "28.15.0",
3
+ "version": "28.17.4",
4
4
  "license": "LGPL-3.0-or-later",
5
5
  "homepage": "https://treb.app",
6
6
  "repository": {
@@ -101,7 +101,7 @@ export interface Dimensions {
101
101
  * sheet class has a method for reducing infinite ranges to actual
102
102
  * populated ranges.
103
103
  */
104
- export class Area implements IArea { // }, IterableIterator<ICellAddress> {
104
+ export class Area implements IArea {
105
105
 
106
106
  // tslint:disable-next-line:variable-name
107
107
  private start_: ICellAddress;
@@ -562,6 +562,7 @@ export class Area implements IArea { // }, IterableIterator<ICellAddress> {
562
562
  return new Area(this.start, this.end); // ensure copies
563
563
  }
564
564
 
565
+ /* removed, use iterator
565
566
  public Array(): ICellAddress[] {
566
567
  if (this.entire_column || this.entire_row) throw new Error('can\'t convert infinite area to array');
567
568
  const array: ICellAddress[] = new Array<ICellAddress>(this.rows * this.columns);
@@ -578,6 +579,7 @@ export class Area implements IArea { // }, IterableIterator<ICellAddress> {
578
579
  }
579
580
  return array;
580
581
  }
582
+ */
581
583
 
582
584
  get left(): Area{
583
585
  const area = new Area(this.start_, this.end_);
@@ -637,6 +639,82 @@ export class Area implements IArea { // }, IterableIterator<ICellAddress> {
637
639
  return this; // fluent
638
640
  }
639
641
 
642
+ /* *
643
+ * preferred to straight iterator. actually in this class iterator
644
+ * is OK but in some other cases we'll want to generate like this
645
+ *
646
+ * eh I don't know about this inline function, is that going to be
647
+ * optimized out? ...
648
+ * /
649
+ public get contents(): Generator<{ row: number, column: number, sheet_id?: number }> {
650
+
651
+ if (this.entire_row || this.entire_column) {
652
+ throw new Error(`don't iterate infinite area`);
653
+ }
654
+
655
+ const sheet_id = this.start_.sheet_id;
656
+ const start_column = this.start_.column;
657
+ const end_column = this.end_.column;
658
+ const start_row = this.start_.row;
659
+ const end_row = this.end_.row;
660
+
661
+
662
+ function *generator() {
663
+ for (let column = start_column; column <= end_column; column++){
664
+ for (let row = start_row; row <= end_row; row++){
665
+ yield {column, row, sheet_id};
666
+ }
667
+ }
668
+ }
669
+
670
+ return generator();
671
+
672
+ }
673
+ */
674
+
675
+ /**
676
+ * modernizing. this is a proper iterator. generators are prettier
677
+ * but there's at least some performance cost -- I'm not sure how
678
+ * much, but it's non-zero.
679
+ */
680
+ public [Symbol.iterator](): Iterator<ICellAddress> {
681
+
682
+ if (this.entire_row || this.entire_column) {
683
+ throw new Error(`don't iterate infinite area`);
684
+ }
685
+
686
+ let row = this.start_.row;
687
+ let column = this.start_.column;
688
+
689
+ // this now uses "live" references, so if the object were mutated
690
+ // during iteration the iterator would reflect those changes. which
691
+ // seems bad, but also correct.
692
+
693
+ return {
694
+ next: () => {
695
+
696
+ const value = { column, row, sheet_id: this.start_.sheet_id };
697
+
698
+ if (column > this.end_.column) {
699
+ return {
700
+ done: true,
701
+ value: undefined,
702
+ };
703
+ }
704
+
705
+ if (++row > this.end_.row) {
706
+ row = this.start_.row;
707
+ column++;
708
+ }
709
+
710
+ return { value };
711
+
712
+ },
713
+ };
714
+
715
+ };
716
+
717
+ /* * @deprecated * /
640
718
  public Iterate(f: (...args: any[]) => any): void {
641
719
  if (this.entire_column || this.entire_row) {
642
720
  console.warn(`don't iterate infinite area`);
@@ -648,6 +726,7 @@ export class Area implements IArea { // }, IterableIterator<ICellAddress> {
648
726
  }
649
727
  }
650
728
  }
729
+ */
651
730
 
652
731
  /* *
653
732
  * testing: we may have to polyfill for IE11, or just not use it at
@@ -796,40 +796,33 @@ export class Cells {
796
796
  return this.GetRange({row: 0, column: 0}, {row: this.rows_ - 1, column: this.columns_ - 1}, transpose);
797
797
  }
798
798
 
799
- /** simply cannot make this work with overloads (prove me wrong) */
800
- public Normalize2(from: ICellAddress, to: ICellAddress): {from: ICellAddress, to: ICellAddress} {
799
+ /** overload */
800
+ public Normalize(from: ICellAddress, to: ICellAddress): { from: ICellAddress, to: ICellAddress };
801
801
 
802
- if (from.column === Infinity) {
803
- from = { ...from, column: 0, };
804
- }
805
-
806
- if (from.row === Infinity) {
807
- from = { ...from, row: 0, };
808
- }
809
-
810
- if (to.column === Infinity) {
811
- to = { ...to, column: this.columns_ - 1};
812
- }
813
-
814
- if (to.row === Infinity) {
815
- to = { ...to, row: this.rows_ - 1};
816
- }
802
+ /** overload */
803
+ public Normalize(from: ICellAddress): { from: ICellAddress };
817
804
 
818
- return {from, to};
819
- }
805
+ /** overload */
806
+ public Normalize<K extends ICellAddress|undefined>(from: ICellAddress, to: K): { from: ICellAddress, to: K };
820
807
 
821
- /** simply cannot make this work with overloads (prove me wrong) */
822
- public Normalize1(from: ICellAddress): ICellAddress {
808
+ /** base */
809
+ public Normalize(from: ICellAddress, to?: ICellAddress): { from: ICellAddress, to?: ICellAddress } {
823
810
 
824
- if (from.column === Infinity) {
825
- from = { ...from, column: 0, };
811
+ from = {
812
+ ...from,
813
+ row: from.row == Infinity ? 0 : from.row,
814
+ column: from.column == Infinity ? 0 : from.column,
826
815
  }
827
816
 
828
- if (from.row === Infinity) {
829
- from = { ...from, row: 0, };
817
+ if (to) {
818
+ to = {
819
+ ...to,
820
+ row: to.row == Infinity ? this.rows_ - 1 : to.row,
821
+ column: to.column == Infinity ? this.columns_ - 1 : to.column,
822
+ }
830
823
  }
831
824
 
832
- return from;
825
+ return { from, to };
833
826
 
834
827
  }
835
828
 
@@ -851,7 +844,7 @@ export class Cells {
851
844
  */
852
845
  public RawValue(from: ICellAddress, to: ICellAddress = from): CellValue | CellValue[][] | undefined {
853
846
 
854
- ({from, to} = this.Normalize2(from, to));
847
+ ({from, to} = this.Normalize(from, to));
855
848
 
856
849
  if (from.row === to.row && from.column === to.column) {
857
850
  if (this.data[from.row] && this.data[from.row][from.column]) {
@@ -885,12 +878,7 @@ export class Cells {
885
878
  /** gets range as values */
886
879
  public GetRange(from: ICellAddress, to?: ICellAddress, transpose = false){
887
880
 
888
- if (to) {
889
- ({from, to} = this.Normalize2(from, to));
890
- }
891
- else {
892
- from = this.Normalize1(from);
893
- }
881
+ ({from, to} = this.Normalize(from, to));
894
882
 
895
883
  // console.info("getrange", from, to, transpose);
896
884
 
@@ -972,7 +960,7 @@ export class Cells {
972
960
 
973
961
  public GetRange4(from: ICellAddress, to: ICellAddress = from, transpose = false): UnionValue {
974
962
 
975
- ({from, to} = this.Normalize2(from, to));
963
+ ({from, to} = this.Normalize(from, to));
976
964
 
977
965
  if (from.row === to.row && from.column === to.column) {
978
966
  if (this.data[from.row] && this.data[from.row][from.column]){
@@ -1008,10 +996,10 @@ export class Cells {
1008
996
 
1009
997
  }
1010
998
 
1011
- /**
999
+ /* *
1012
1000
  * apply function to address/area
1013
1001
  * @deprecated - use Apply2
1014
- */
1002
+ * /
1015
1003
  public Apply(area: Area|ICellAddress, f: (cell: Cell, c?: number, r?: number) => void, create_missing_cells = false): void {
1016
1004
 
1017
1005
  // allow single address
@@ -1050,12 +1038,71 @@ export class Cells {
1050
1038
  }
1051
1039
  }
1052
1040
  }
1041
+ */
1053
1042
 
1054
- /**
1043
+ /* *
1044
+ * as a replacement for the Apply functions. testing
1045
+ *
1046
+ * (1) it seems like almost no one uses the cell address, so returning
1047
+ * it just adds a wasteful destructuring to every loop iteration.
1048
+ *
1049
+ * actually there's exactly one, and we could work around it. but
1050
+ * it seems like it might be useful in some cases, so... ?
1051
+ *
1052
+ * (2) can we consolidate with the IterateAll method (named Iterate)?
1053
+ * that would only work if we had the same return type, meaning either
1054
+ * drop the address from this one or add it to the other one
1055
+ *
1056
+ * @deprecated use Iterate, if possible
1057
+ *
1058
+ * /
1059
+ public *IterateArea(area: Area|ICellAddress, create_missing_cells = false) {
1060
+
1061
+ // allow single address
1062
+ if (IsCellAddress(area)) {
1063
+ area = new Area(area);
1064
+ }
1065
+
1066
+ // why not just cap? (...)
1067
+ if (area.entire_column || area.entire_row) {
1068
+ throw new Error(`don't iterate infinite cells`);
1069
+ }
1070
+
1071
+ const start = area.start;
1072
+ const end = area.end;
1073
+
1074
+ if (create_missing_cells){
1075
+ for ( let r = start.row; r <= end.row; r++ ){
1076
+ if (!this.data[r]) this.data[r] = [];
1077
+ const row = this.data[r];
1078
+ for ( let c = start.column; c <= end.column; c++ ){
1079
+ if (!row[c]) row[c] = new Cell();
1080
+ yield { row: r, column: c, cell: row[c] };
1081
+ }
1082
+ }
1083
+ }
1084
+ else {
1085
+ // we can loop over indexes that don't exist, just check for existence
1086
+ for ( let r = start.row; r <= end.row; r++ ){
1087
+ if (this.data[r]){
1088
+ const row = this.data[r];
1089
+ for ( let c = start.column; c <= end.column; c++ ){
1090
+ if (row[c]) {
1091
+ yield { row: r, column: c, cell: row[c] };
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ }
1099
+ */
1100
+
1101
+ /* *
1055
1102
  * apply function to address/area
1056
1103
  *
1057
1104
  * this version lets you abort by returning false from the callback function
1058
- */
1105
+ * /
1059
1106
  public Apply2(area: Area|ICellAddress, func: (cell: Cell, c?: number, r?: number) => boolean, create_missing_cells = false): void {
1060
1107
 
1061
1108
  // allow single address
@@ -1100,6 +1147,7 @@ export class Cells {
1100
1147
  }
1101
1148
  }
1102
1149
  }
1150
+ */
1103
1151
 
1104
1152
  /**
1105
1153
  * set area. shortcut to reduce overhead. consolidates single value
@@ -1148,11 +1196,108 @@ export class Cells {
1148
1196
  }
1149
1197
 
1150
1198
  /**
1199
+ * replacement for old callback iterator. this is a composite
1200
+ * of iterating all and iterating an area. I want to remove the
1201
+ * other method, but there are still some calls that use the
1202
+ * cell address.
1203
+ *
1204
+ * Q: is it possible to use Symbol.iterator with arguments?
1205
+ * A: apparently it is, but how would you call it? (...)
1206
+ */
1207
+ public *Iterate(area?: Area|ICellAddress, create_missing_cells = false) {
1208
+
1209
+ // special case. normally iterating over all cells
1210
+ // doesn't create missing, so we use a simpler loop.
1211
+
1212
+ if (!area && create_missing_cells) {
1213
+ area = new Area({
1214
+ row: 0,
1215
+ column: 0,
1216
+ }, {
1217
+ row: this.rows_ - 1,
1218
+ column: this.columns - 1,
1219
+ });
1220
+ }
1221
+
1222
+ // if we have an area, iterate over the area. we need indexes.
1223
+
1224
+ if (area) {
1225
+
1226
+ // allow single address
1227
+ if (IsCellAddress(area)) {
1228
+ area = new Area(area);
1229
+ }
1230
+
1231
+ // why not just cap? (...)
1232
+ // if (area.entire_column || area.entire_row) {
1233
+ // throw new Error(`don't iterate infinite cells`);
1234
+ //}
1235
+
1236
+ if (area.entire_column || area.entire_row) {
1237
+ area = new Area(area.start, area.end);
1238
+ if (area.start.column === Infinity) {
1239
+ area.start.column = 0;
1240
+ area.end.column = this.columns_ - 1;
1241
+ }
1242
+ if (area.start.row === Infinity) {
1243
+ area.start.row = 0;
1244
+ area.end.row = this.rows_ - 1;
1245
+ }
1246
+ }
1247
+
1248
+ const start = area.start;
1249
+ const end = area.end;
1250
+
1251
+ if (create_missing_cells){
1252
+ for ( let r = start.row; r <= end.row; r++ ){
1253
+ if (!this.data[r]) this.data[r] = [];
1254
+ const row = this.data[r];
1255
+ for ( let c = start.column; c <= end.column; c++ ){
1256
+ if (!row[c]) row[c] = new Cell();
1257
+ yield row[c]; // { row: r, column: c, cell: row[c] };
1258
+ }
1259
+ }
1260
+ }
1261
+ else {
1262
+ // we can loop over indexes that don't exist, just check for existence
1263
+ for ( let r = start.row; r <= end.row; r++ ){
1264
+ if (this.data[r]){
1265
+ const row = this.data[r];
1266
+ for ( let c = start.column; c <= end.column; c++ ){
1267
+ if (row[c]) {
1268
+ yield row[c]; // { row: r, column: c, cell: row[c] };
1269
+ }
1270
+ }
1271
+ }
1272
+ }
1273
+ }
1274
+
1275
+ }
1276
+ else {
1277
+
1278
+ // no area; just iterate all cells. implicitly skip undefined cells.
1279
+
1280
+ for (const row of this.data) {
1281
+ if (row) {
1282
+ for (const cell of row) {
1283
+ if (cell) {
1284
+ yield cell;
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+
1292
+ /* *
1151
1293
  * iterates over all cells (using loops) and runs function per-cell.
1152
1294
  * FIXME: switch to indexing on empty indexes? (...)
1153
- */
1295
+ *
1296
+ * removed in favor of generator-based function
1297
+ *
1298
+ * /
1154
1299
  public IterateAll(func: (cell: Cell) => void){
1155
- /*
1300
+ / *
1156
1301
  const row_keys = Object.keys(this.data);
1157
1302
  for (const row of row_keys){
1158
1303
  const n_row = Number(row) || 0;
@@ -1161,7 +1306,7 @@ export class Cells {
1161
1306
  f(this.data[n_row][Number(column_key)]);
1162
1307
  }
1163
1308
  }
1164
- */
1309
+ * /
1165
1310
  for (const row of this.data) {
1166
1311
  if (row) {
1167
1312
  for (const cell of row) {
@@ -1173,6 +1318,7 @@ export class Cells {
1173
1318
  }
1174
1319
 
1175
1320
  }
1321
+ */
1176
1322
 
1177
1323
  /** moved from sheet, so we can do it non-functional style (for perf) */
1178
1324
  public FlushCellStyles() {
@@ -2475,9 +2475,15 @@ export class Calculator extends Graph {
2475
2475
  const unit = dependencies.ranges[key];
2476
2476
  const range = new Area(unit.start, unit.end);
2477
2477
 
2478
+ for (const address of range) {
2479
+ this.AddLeafVertexEdge(address, vertex);
2480
+ }
2481
+
2482
+ /*
2478
2483
  range.Iterate((address: ICellAddress) => {
2479
2484
  this.AddLeafVertexEdge(address, vertex);
2480
2485
  });
2486
+ */
2481
2487
 
2482
2488
  /*
2483
2489
  for (const address of range) {
@@ -2709,7 +2715,13 @@ export class Calculator extends Graph {
2709
2715
  this.AddArrayEdge(range, address);
2710
2716
  }
2711
2717
  else {
2712
- range.Iterate((target: ICellAddress) => this.AddEdge(target, address));
2718
+
2719
+ for (const target of range) {
2720
+ this.AddEdge(target, address);
2721
+ }
2722
+
2723
+ // range.Iterate((target: ICellAddress) => this.AddEdge(target, address));
2724
+
2713
2725
  }
2714
2726
 
2715
2727
 
@@ -729,6 +729,77 @@ export const BaseFunctionLibrary: FunctionMap = {
729
729
  })
730
730
  },
731
731
 
732
+ Large: {
733
+ description: 'Returns the nth numeric value from the data, in descending order',
734
+ arguments: [
735
+ {
736
+ name: 'values',
737
+ },
738
+ {
739
+ name: 'index',
740
+ }
741
+ ],
742
+
743
+ // OK a little tricky here -- we're going to reverse the arguments
744
+ // so we can call apply as array, but applying against the trailing
745
+ // arguments.
746
+
747
+ fn: Utils.ApplyAsArraySwap((data: any, index: number) => {
748
+
749
+ if (index <= 0) {
750
+ return ArgumentError();
751
+ }
752
+
753
+ const flat = Utils.FlattenUnboxed(data);
754
+ const numeric: number[] = flat.filter(test => typeof test === 'number');
755
+ numeric.sort((a, b) => b - a);
756
+
757
+ if (index <= numeric.length) {
758
+ return {
759
+ type: ValueType.number,
760
+ value: numeric[index - 1],
761
+ };
762
+ }
763
+
764
+ return ArgumentError();
765
+ }),
766
+ },
767
+
768
+ Small: {
769
+ description: 'Returns the nth numeric value from the data, in ascending order',
770
+ arguments: [
771
+ {
772
+ name: 'values',
773
+ },
774
+ {
775
+ name: 'index',
776
+ }
777
+ ],
778
+
779
+ // see large, above
780
+
781
+ fn: Utils.ApplyAsArraySwap((data: any, index: number) => {
782
+
783
+ if (index <= 0) {
784
+ return ArgumentError();
785
+ }
786
+
787
+ const flat = Utils.FlattenUnboxed(data);
788
+ const numeric: number[] = flat.filter(test => typeof test === 'number');
789
+ numeric.sort((a, b) => a - b);
790
+
791
+ if (index <= numeric.length) {
792
+ return {
793
+ type: ValueType.number,
794
+ value: numeric[index - 1],
795
+ };
796
+ }
797
+
798
+ return ArgumentError();
799
+
800
+ }),
801
+ },
802
+
732
803
  /**
733
804
  * sort arguments, but ensure we return empty strings to
734
805
  * fill up the result array
@@ -193,22 +193,52 @@ export const ApplyArrayFunc = (base: (...args: any[]) => any) => {
193
193
  };
194
194
  };
195
195
 
196
+ export const ApplyAsArraySwap = (base: (...args: any[]) => UnionValue) => {
197
+ return (...args: any[]): UnionValue => {
198
+
199
+ // swap here
200
+ args.reverse();
201
+ const [a, ...rest] = args;
202
+
203
+ if (Array.isArray(a)) {
204
+ return {
205
+ type: ValueType.array,
206
+ value: a.map(row => row.map((element: any) => {
207
+
208
+ // swap back
209
+ const swapped = [...rest, element];
210
+ return base(...swapped);
211
+
212
+ })),
213
+ };
214
+ }
215
+ else if (typeof a === 'object' && !!a && a.type === ValueType.array ) {
216
+ return {
217
+ type: ValueType.array,
218
+ value: (a as ArrayUnion).value.map(row => row.map((element: any) => {
219
+
220
+ const swapped = [...rest, element];
221
+ return base(...swapped);
222
+
223
+ })),
224
+ };
225
+
226
+ }
227
+ else {
228
+ return base(...rest, a);
229
+ }
230
+ }
231
+ };
232
+
196
233
  export const ApplyAsArray = (base: (a: any, ...rest: any[]) => UnionValue) => {
197
234
  return (a: any, ...rest: any[]): UnionValue => {
198
235
  if (Array.isArray(a)) {
199
-
200
236
  return {
201
237
  type: ValueType.array,
202
238
  value: a.map(row => row.map((element: any) => {
203
239
  return base(element, ...rest);
204
240
  })),
205
241
  };
206
-
207
- /*
208
- return a.map(row => row.map((element: any) => {
209
- return base(element, ...rest);
210
- }));
211
- */
212
242
  }
213
243
  else if (typeof a === 'object' && !!a && a.type === ValueType.array ) {
214
244
  return {
@@ -280,23 +310,6 @@ export const ApplyAsArray2 = (base: (a: any, b: any, ...rest: any[]) => UnionVal
280
310
  };
281
311
  }
282
312
 
283
- /*
284
- if (Array.isArray(a)) {
285
- if (Array.isArray(b)) {
286
- return a.map((row, i: number) => row.map((element: any, j: number) => {
287
- return base(element, b[i][j], ...rest);
288
- }));
289
- }
290
- else {
291
- return a.map(row => row.map((element: any) => {
292
- return base(element, b, ...rest);
293
- }));
294
- }
295
- }
296
- else {
297
- return base(a, b, ...rest);
298
- }
299
- */
300
313
  return base(a, b, ...rest);
301
314
 
302
315
  }
@@ -47,6 +47,11 @@
47
47
  <button data-command="align-bottom" title="Align to bottom"></button>
48
48
  </div>
49
49
 
50
+ <div class="group" wide indent-group>
51
+ <button data-command="outdent" title="Decrease indent"></button>
52
+ <button data-command="indent" title="Incrase indent"></button>
53
+ </div>
54
+
50
55
  <div class="group">
51
56
  <button data-command="wrap-text" title="Wrap text"></button>
52
57
  <button data-command="merge-cells" data-id="merge" data-inactive-title="Merge cells" data-active-title="Unmerge cells"></button>
@@ -146,6 +146,7 @@ export class SpreadsheetConstructor<USER_DATA_TYPE = unknown> {
146
146
  if (this.root) {
147
147
 
148
148
  const names = this.root.getAttributeNames();
149
+ console.info({names});
149
150
 
150
151
  for (let name of names) {
151
152
 
@@ -202,8 +203,7 @@ export class SpreadsheetConstructor<USER_DATA_TYPE = unknown> {
202
203
  // attrtibute options are in kebab-case while our internal
203
204
  // options are still in snake_case.
204
205
 
205
- name = name.replace(/-/g, '_');
206
- attribute_options[name] = this.CoerceAttributeValue(this.root.getAttribute(name));
206
+ attribute_options[name.replace(/-/g, '_')] = this.CoerceAttributeValue(this.root.getAttribute(name));
207
207
 
208
208
  }
209
209
  }
@@ -910,6 +910,9 @@ export class SpreadsheetConstructor<USER_DATA_TYPE = unknown> {
910
910
  if (!sheet.options.file_menu) {
911
911
  remove.push(toolbar.querySelector('[file-menu]'));
912
912
  }
913
+ if (!sheet.options.indent_buttons) {
914
+ remove.push(toolbar.querySelector('[indent-group]'));
915
+ }
913
916
  if (!sheet.options.font_scale) {
914
917
  remove.push(toolbar.querySelector('[font-scale]'));
915
918
  }