@trebco/treb 30.16.0 → 31.0.2

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 (47) hide show
  1. package/api-generator/api-generator.ts +3 -1
  2. package/api-generator/package.json +2 -1
  3. package/dist/treb-export-worker.mjs +2 -2
  4. package/dist/treb-spreadsheet.mjs +13 -13
  5. package/dist/treb.d.ts +19 -2
  6. package/package.json +8 -7
  7. package/treb-base-types/src/font-stack.ts +144 -0
  8. package/treb-base-types/src/style.ts +121 -11
  9. package/treb-base-types/src/theme.ts +53 -8
  10. package/treb-calculator/src/calculator.ts +13 -13
  11. package/treb-calculator/src/descriptors.ts +12 -4
  12. package/treb-calculator/src/expression-calculator.ts +17 -4
  13. package/treb-calculator/src/functions/base-functions.ts +57 -4
  14. package/treb-calculator/src/functions/statistics-functions.ts +9 -6
  15. package/treb-calculator/tsconfig.json +11 -0
  16. package/treb-charts/style/charts.scss +7 -1
  17. package/treb-data-model/src/annotation.ts +6 -0
  18. package/treb-data-model/src/data_model.ts +14 -3
  19. package/treb-data-model/src/sheet.ts +57 -56
  20. package/treb-embed/markup/toolbar.html +15 -1
  21. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +38 -0
  22. package/treb-embed/src/embedded-spreadsheet.ts +119 -29
  23. package/treb-embed/src/options.ts +3 -0
  24. package/treb-embed/src/selection-state.ts +1 -0
  25. package/treb-embed/src/toolbar-message.ts +6 -0
  26. package/treb-embed/src/types.ts +9 -0
  27. package/treb-embed/style/defaults.scss +12 -1
  28. package/treb-embed/style/font-stacks.scss +105 -0
  29. package/treb-embed/style/layout.scss +1 -0
  30. package/treb-embed/style/theme-defaults.scss +12 -2
  31. package/treb-embed/style/toolbar.scss +16 -0
  32. package/treb-grid/src/editors/overlay_editor.ts +36 -3
  33. package/treb-grid/src/layout/base_layout.ts +52 -37
  34. package/treb-grid/src/layout/grid_layout.ts +7 -0
  35. package/treb-grid/src/render/tile_renderer.ts +154 -148
  36. package/treb-grid/src/types/grid.ts +188 -54
  37. package/treb-grid/src/types/grid_events.ts +1 -1
  38. package/treb-grid/src/types/grid_options.ts +3 -0
  39. package/treb-grid/src/util/fontmetrics.ts +134 -0
  40. package/treb-parser/src/parser.ts +12 -3
  41. package/treb-utils/src/measurement.ts +2 -3
  42. package/tsproject.json +1 -1
  43. package/treb-calculator/modern.tsconfig.json +0 -11
  44. package/treb-grid/src/util/fontmetrics2.ts +0 -182
  45. package/treb-parser/src/parser.test.ts +0 -298
  46. /package/treb-embed/{modern.tsconfig.json → tsconfig.json} +0 -0
  47. /package/treb-export/{modern.tsconfig.json → tsconfig.json} +0 -0
@@ -30,11 +30,10 @@ import type {
30
30
  Complex,
31
31
  Color,
32
32
  CellStyle,
33
- IRectangle} from 'treb-base-types';
33
+ IRectangle } from 'treb-base-types';
34
34
 
35
35
  import {
36
36
  Area,
37
- Style,
38
37
  Is2DArray,
39
38
  Rectangle,
40
39
  ValueType,
@@ -46,6 +45,7 @@ import {
46
45
  IsComplex,
47
46
  TextPartFlag,
48
47
  IsArea,
48
+ Style,
49
49
  } from 'treb-base-types';
50
50
 
51
51
  import type { ExpressionUnit, RenderOptions, UnitAddress } from 'treb-parser';
@@ -139,6 +139,14 @@ export class Grid extends GridBase {
139
139
  // new...
140
140
  public headless = false;
141
141
 
142
+ /**
143
+ * we're tracking the current selected style so we can use it for new
144
+ * cells. conceptually, if you are typing in a font (stack) like handwritten,
145
+ * and you enter a new cell, you probably want to type in the same font
146
+ * there. so we want to make that happen. TODO: size
147
+ */
148
+ public readonly edit_state: CellStyle = {};
149
+
142
150
  public get scale(): number {
143
151
  return this.layout.scale;
144
152
  }
@@ -148,7 +156,7 @@ export class Grid extends GridBase {
148
156
  this.layout.scale = value;
149
157
  this.UpdateLayout();
150
158
  this.UpdateAnnotationLayout();
151
- this.layout.UpdateAnnotation(this.active_sheet.annotations);
159
+ this.layout.UpdateAnnotation(this.active_sheet.annotations, this.theme);
152
160
  this.layout.ApplyTheme(this.theme);
153
161
  this.overlay_editor?.UpdateScale(value);
154
162
  this.tab_bar?.UpdateScale(value);
@@ -173,6 +181,18 @@ export class Grid extends GridBase {
173
181
  */
174
182
  public readonly theme: Theme; // ExtendedTheme;
175
183
 
184
+ /**
185
+ * this was private, which made sense, but there's a case where the
186
+ * client (embedded sheet) wants to check if an annotation is selected.
187
+ * we need to allow that somehow, ideally without any reacharounds.
188
+ *
189
+ * I guess the concern is a client could modify it, but at this point
190
+ * we really only have one client and we trust it. making this public.
191
+ * we could maybe switch to an accessor or have a "is this selected?" method.
192
+ */
193
+ public selected_annotation?: Annotation;
194
+
195
+
176
196
  // --- private members -------------------------------------------------------
177
197
 
178
198
  // testing
@@ -196,9 +216,6 @@ export class Grid extends GridBase {
196
216
  /** */
197
217
  private editing_selection: GridSelection|undefined;
198
218
 
199
- /** */
200
- private selected_annotation?: Annotation;
201
-
202
219
  /** */
203
220
  private editing_annotation?: Annotation;
204
221
 
@@ -787,16 +804,33 @@ export class Grid extends GridBase {
787
804
  if (event) {
788
805
  this.grid_events.Publish(event);
789
806
  }
790
-
807
+ else {
808
+
809
+ // probably a click on the annotation. if it is not already
810
+ // selected, send an event.
811
+
812
+ if (this.selected_annotation !== annotation) {
813
+ this.grid_events.Publish({
814
+ type: 'annotation',
815
+ annotation,
816
+ event: 'select',
817
+ })
818
+ }
819
+
820
+ }
821
+
791
822
  if (annotation.data.layout) {
792
823
  this.EnsureAddress(annotation.data.layout.br.address, 1);
793
824
  }
794
825
 
795
826
  });
827
+
796
828
  },
797
829
 
798
830
  focusin: () => {
799
831
 
832
+ // console.info("AFI");
833
+
800
834
  for (const element of this.layout.GetFrozenAnnotations(annotation)) {
801
835
  element.classList.add('clone-focus');
802
836
  }
@@ -816,7 +850,9 @@ export class Grid extends GridBase {
816
850
 
817
851
  focusout: (event) => {
818
852
 
819
- // console.info('annotation focusout', annotation);
853
+ // console.info("AFO");
854
+
855
+ // console.info('annotation focusout', annotation, event);
820
856
 
821
857
  for (const element of this.layout.GetFrozenAnnotations(annotation)) {
822
858
  element.classList.remove('clone-focus');
@@ -838,10 +874,24 @@ export class Grid extends GridBase {
838
874
 
839
875
  }
840
876
  else {
841
- if (this.selected_annotation === annotation) {
842
- this.selected_annotation = undefined;
877
+
878
+ // here's where we need to make a change. if the new focus
879
+ // (the related target) is outside of the sheet hierarchy, we
880
+ // want to persist the selection, or at least remember it.
881
+
882
+ // the next time we focus in on the grid we want to have the
883
+ // opportunity to restore this focus. I think persisting the
884
+ // focus may be a problem? not sure...
885
+
886
+ const focus_in_layout = this.layout.FocusInLayout(event.relatedTarget||undefined);
887
+
888
+ if (focus_in_layout) {
889
+ if (this.selected_annotation === annotation) {
890
+ this.selected_annotation = undefined;
891
+ }
892
+ this.ShowGridSelection();
843
893
  }
844
- this.ShowGridSelection();
894
+
845
895
  }
846
896
  },
847
897
 
@@ -965,7 +1015,7 @@ export class Grid extends GridBase {
965
1015
  }
966
1016
 
967
1017
  if (add_to_layout) {
968
- this.layout.AddAnnotation(annotation);
1018
+ this.layout.AddAnnotation(annotation, this.theme);
969
1019
  if (annotation.data.layout) {
970
1020
  this.EnsureAddress(annotation.data.layout.br.address, 1, toll_events);
971
1021
  }
@@ -1356,7 +1406,7 @@ export class Grid extends GridBase {
1356
1406
  let composite: Theme = JSON.parse(JSON.stringify(DefaultTheme));
1357
1407
 
1358
1408
  if (this.view_node) {
1359
- const theme_properties = LoadThemeProperties(this.view_node);
1409
+ const theme_properties = LoadThemeProperties(this.view_node, this.options.support_font_stacks);
1360
1410
  composite = {...theme_properties};
1361
1411
  }
1362
1412
 
@@ -1396,7 +1446,7 @@ export class Grid extends GridBase {
1396
1446
  // update style for theme
1397
1447
  this.StyleDefaultFromTheme();
1398
1448
 
1399
- this.active_sheet.UpdateDefaultRowHeight();
1449
+ this.active_sheet.UpdateDefaultRowHeight(this.theme);
1400
1450
  this.active_sheet.FlushCellStyles();
1401
1451
 
1402
1452
  this.layout.ApplyTheme(this.theme);
@@ -1646,10 +1696,62 @@ export class Grid extends GridBase {
1646
1696
  }
1647
1697
 
1648
1698
  /**
1649
- * focus on the container. you must call this method to get copying
1650
- * to work properly (because it creates a selection)
1699
+ *
1700
+ */
1701
+ public ApplyAnnotationStyle(style: CellStyle = {}, delta = true) {
1702
+
1703
+ // get the logic from committing a formula, I guess? it should run
1704
+ // through the command queue to update any related views.
1705
+ //
1706
+ // actually, FIXME? I think updating annotations generally is not
1707
+ // running through the command queue, that's a larger issue we need
1708
+ // to look at.
1709
+
1710
+ if (this.selected_annotation) {
1711
+
1712
+ const annotation = this.selected_annotation;
1713
+ annotation.data.style = JSON.parse(JSON.stringify(
1714
+ delta ? Style.Composite([annotation.data.style || {}, style]) : style
1715
+ ));
1716
+ const node = annotation.view[this.view_index]?.node;
1717
+
1718
+ this.layout.UpdateAnnotation(annotation, this.theme);
1719
+
1720
+ if (node) {
1721
+ node.focus();
1722
+ }
1723
+ this.grid_events.Publish({ type: 'annotation', event: 'update', annotation });
1724
+ this.DelayedRender();
1725
+ }
1726
+
1727
+ }
1728
+
1729
+ /**
1730
+ *
1731
+ */
1732
+ public AnnotationSelected() {
1733
+ return !!this.selected_annotation;
1734
+ }
1735
+
1736
+ /**
1737
+ * focus on the container. not sure what that text parameter was,
1738
+ * legacy? TODO: remove
1651
1739
  */
1652
- public Focus(text = ''): void {
1740
+ public Focus(text = '', click = false): void {
1741
+
1742
+ if (this.selected_annotation) {
1743
+
1744
+ if (click) {
1745
+ this.selected_annotation = undefined;
1746
+ this.ShowGridSelection();
1747
+ }
1748
+ else {
1749
+ // console.info("reselect annotation...", this.selected_annotation);
1750
+ // console.info("check", this.view_index, this.selected_annotation.view[this.view_index].node);
1751
+ this.selected_annotation.view[this.view_index].node?.focus();
1752
+ return;
1753
+ }
1754
+ }
1653
1755
 
1654
1756
  // FIXME: cache a pointer
1655
1757
  if (UA.is_mobile) {
@@ -2465,40 +2567,55 @@ export class Grid extends GridBase {
2465
2567
  this.theme.grid_cell?.font_size || { unit: 'pt', value: 10 };
2466
2568
  }
2467
2569
 
2570
+ private AutoSizeRow(sheet: Sheet, row: number, allow_shrink = true): void {
2571
+
2572
+ if (!this.tile_renderer) {
2573
+ return;
2574
+ }
2575
+
2576
+ const current_height = sheet.GetRowHeight(row);
2577
+ let max_height = 0;
2578
+ const padding = 6; // 4 * 2; // FIXME: parameterize
2579
+
2580
+ for (let column = 0; column < sheet.cells.columns; column++) {
2581
+ const cell = sheet.CellData({ row, column });
2582
+ const { height } = this.tile_renderer.MeasureText(cell, sheet.GetColumnWidth(column), 1);
2583
+ max_height = Math.max(max_height, height + padding);
2584
+ }
2585
+
2586
+ if (!allow_shrink) {
2587
+ max_height = Math.max(current_height, max_height);
2588
+ }
2589
+
2590
+ if (max_height > padding + 2) {
2591
+ sheet.SetRowHeight(row, max_height);
2592
+ }
2593
+
2594
+
2595
+ }
2596
+
2468
2597
  private AutoSizeColumn(sheet: Sheet, column: number, allow_shrink = true): void {
2469
2598
 
2470
2599
  if (!this.tile_renderer) {
2471
2600
  return;
2472
2601
  }
2473
2602
 
2474
- // const context = Sheet.measurement_canvas.getContext('2d');
2475
- // if (!context) return;
2476
-
2477
- let width = 0;
2603
+ const current_width = sheet.GetColumnWidth(column);
2604
+ let max_width = 0;
2478
2605
  const padding = 12; // 4 * 2; // FIXME: parameterize
2479
2606
 
2480
- if (!allow_shrink) width = sheet.GetColumnWidth(column);
2481
-
2482
2607
  for (let row = 0; row < sheet.cells.rows; row++) {
2483
-
2484
2608
  const cell = sheet.CellData({ row, column });
2485
- let text = cell.formatted || '';
2486
- if (typeof text !== 'string') {
2487
- text = text.map((part) => part.text).join('');
2488
- }
2489
-
2490
- if (text && text.length) {
2491
- const metrics = this.tile_renderer.MeasureText(text, Style.Font(cell.style || {}));
2609
+ const { width } = this.tile_renderer.MeasureText(cell, current_width, 1);
2610
+ max_width = Math.max(max_width, width + padding);
2611
+ }
2492
2612
 
2493
- // context.font = Style.Font(cell.style || {});
2494
- // console.info({text, style: Style.Font(cell.style||{}), cf: context.font});
2495
- // width = Math.max(width, Math.ceil(context.measureText(text).width) + padding);
2496
- width = Math.max(width, Math.ceil(metrics.width) + padding);
2497
- }
2613
+ if (!allow_shrink) {
2614
+ max_width = Math.max(current_width, max_width);
2498
2615
  }
2499
2616
 
2500
- if (width > padding + 2) {
2501
- sheet.SetColumnWidth(column, width);
2617
+ if (max_width > padding + 2) {
2618
+ sheet.SetColumnWidth(column, max_width);
2502
2619
  }
2503
2620
 
2504
2621
  }
@@ -3074,7 +3191,7 @@ export class Grid extends GridBase {
3074
3191
  }
3075
3192
 
3076
3193
  this.render_tiles = this.layout.VisibleTiles();
3077
- this.layout.UpdateAnnotation(this.active_sheet.annotations);
3194
+ this.layout.UpdateAnnotation(this.active_sheet.annotations, this.theme);
3078
3195
 
3079
3196
  // FIXME: why is this here, as opposed to coming from the command
3080
3197
  // exec method? are we doubling up? (...)
@@ -3369,7 +3486,7 @@ export class Grid extends GridBase {
3369
3486
 
3370
3487
  this.layout.UpdateTileHeights(true, row);
3371
3488
  this.Repaint(false, true); // repaint full tiles
3372
- this.layout.UpdateAnnotation(this.active_sheet.annotations);
3489
+ this.layout.UpdateAnnotation(this.active_sheet.annotations, this.theme);
3373
3490
 
3374
3491
  });
3375
3492
 
@@ -3451,7 +3568,7 @@ export class Grid extends GridBase {
3451
3568
  }
3452
3569
  */
3453
3570
  if (!this.SelectingArgument()) {
3454
- this.Focus();
3571
+ this.Focus(undefined, true);
3455
3572
  }
3456
3573
 
3457
3574
 
@@ -3592,7 +3709,8 @@ export class Grid extends GridBase {
3592
3709
  x: tooltip_base + delta,
3593
3710
  });
3594
3711
 
3595
- // tile_sizes[tile_index] = tile_width + delta;
3712
+ // I don't get how this works. it's not scaling?
3713
+
3596
3714
  this.layout.SetColumnWidth(column, width);
3597
3715
 
3598
3716
  for (const { annotation, x } of move_annotation_list) {
@@ -3679,7 +3797,7 @@ export class Grid extends GridBase {
3679
3797
  this.ExecCommand({
3680
3798
  key: CommandKey.ResizeColumns,
3681
3799
  column: columns,
3682
- width: width / this.scale,
3800
+ width: width, // / this.scale,
3683
3801
  });
3684
3802
 
3685
3803
  for (const { annotation } of move_annotation_list) {
@@ -3710,7 +3828,7 @@ export class Grid extends GridBase {
3710
3828
  // @see Mousedown_RowHeader
3711
3829
 
3712
3830
  if (!this.SelectingArgument()) {
3713
- this.Focus();
3831
+ this.Focus(undefined, true);
3714
3832
  }
3715
3833
 
3716
3834
  /*
@@ -4023,7 +4141,7 @@ export class Grid extends GridBase {
4023
4141
 
4024
4142
  // not sure why this breaks the formula bar handler
4025
4143
 
4026
- this.Focus();
4144
+ this.Focus(undefined, true);
4027
4145
 
4028
4146
  }
4029
4147
 
@@ -4711,7 +4829,7 @@ export class Grid extends GridBase {
4711
4829
  // let's support command+shift+enter on mac
4712
4830
  const array = (event.key === 'Enter' && (event.ctrlKey || (UA.is_mac && event.metaKey)) && event.shiftKey);
4713
4831
 
4714
- this.SetInferredType(this.overlay_editor.selection, value, array);
4832
+ this.SetInferredType(this.overlay_editor.selection, value, array, undefined, this.overlay_editor.edit_style);
4715
4833
  }
4716
4834
 
4717
4835
  this.DismissEditor();
@@ -5215,7 +5333,7 @@ export class Grid extends GridBase {
5215
5333
  * of commands. the former is the default for editor commits; the latter
5216
5334
  * is used for paste.
5217
5335
  */
5218
- private SetInferredType(selection: GridSelection, value: string|undefined, array = false, exec = true) {
5336
+ private SetInferredType(selection: GridSelection, value: string|undefined, array = false, exec = true, apply_style?: CellStyle) {
5219
5337
 
5220
5338
  // validation: cannot change part of an array without changing the
5221
5339
  // whole array. so check the array. separately, if you are entering
@@ -5293,10 +5411,19 @@ export class Grid extends GridBase {
5293
5411
 
5294
5412
  if (cell.merge_area) target = cell.merge_area.start; // this probably can't happen at this point
5295
5413
 
5414
+ const commands: Command[] = [];
5415
+
5416
+ if (apply_style) {
5417
+ commands.push({
5418
+ key: CommandKey.UpdateStyle,
5419
+ style: apply_style,
5420
+ area: array ? selection.area : selection.target,
5421
+ })
5422
+ }
5423
+
5296
5424
  // first check functions
5297
5425
 
5298
5426
  const is_function = (typeof value === 'string' && value.trim()[0] === '=');
5299
- const commands: Command[] = [];
5300
5427
 
5301
5428
  if (is_function) {
5302
5429
 
@@ -5828,6 +5955,12 @@ export class Grid extends GridBase {
5828
5955
 
5829
5956
  let cell_value = cell.value;
5830
5957
 
5958
+ let edit_state: CellStyle|undefined;
5959
+
5960
+ if (typeof cell_value === 'undefined' && !cell.style?.font_face) {
5961
+ edit_state = this.edit_state;
5962
+ }
5963
+
5831
5964
  // if called from a keypress, we will overwrite whatever is in there so we
5832
5965
  // can just leave text as is -- except for handling %, which needs to get
5833
5966
  // injected.
@@ -5855,7 +5988,7 @@ export class Grid extends GridBase {
5855
5988
  // if so, that should go in the method.
5856
5989
 
5857
5990
  // this.overlay_editor?.Edit(selection, rect.Shift(-1, -1).Expand(1, 1), cell, cell_value, event);
5858
- this.overlay_editor?.Edit(selection, rect.Expand(-1, -1), cell, cell_value, event);
5991
+ this.overlay_editor?.Edit(selection, rect.Expand(-1, -1), cell, cell_value, event, edit_state);
5859
5992
 
5860
5993
  cell.editing = true;
5861
5994
  cell.render_clean = [];
@@ -7388,7 +7521,7 @@ export class Grid extends GridBase {
7388
7521
  this.Repaint();
7389
7522
 
7390
7523
  if (result.update_annotations_list?.length) {
7391
- this.layout.UpdateAnnotation(result.update_annotations_list);
7524
+ this.layout.UpdateAnnotation(result.update_annotations_list, this.theme);
7392
7525
  for (const annotation of result.resize_annotations_list || []) {
7393
7526
  const view = annotation.view[this.view_index];
7394
7527
  if (view?.resize_callback) {
@@ -7472,7 +7605,7 @@ export class Grid extends GridBase {
7472
7605
  this.DelayedRender(true, undefined, true);
7473
7606
 
7474
7607
  if (result.update_annotations_list?.length) {
7475
- this.layout.UpdateAnnotation(result.update_annotations_list);
7608
+ this.layout.UpdateAnnotation(result.update_annotations_list, this.theme);
7476
7609
  for (const annotation of result.resize_annotations_list || []) {
7477
7610
  const view = annotation.view[this.view_index];
7478
7611
  if (view?.resize_callback) {
@@ -7552,7 +7685,7 @@ export class Grid extends GridBase {
7552
7685
  this.Repaint(false, true); // repaint full tiles
7553
7686
  }
7554
7687
 
7555
- this.layout.UpdateAnnotation(this.active_sheet.annotations);
7688
+ this.layout.UpdateAnnotation(this.active_sheet.annotations, this.theme);
7556
7689
  this.RenderSelections();
7557
7690
 
7558
7691
  }
@@ -7618,7 +7751,8 @@ export class Grid extends GridBase {
7618
7751
  }
7619
7752
  }
7620
7753
 
7621
- sheet.AutoSizeRow(entry, this.theme.grid_cell, shrink);
7754
+ // sheet.AutoSizeRow(entry, this.theme, shrink, this.scale);
7755
+ this.AutoSizeRow(sheet, entry, shrink);
7622
7756
  }
7623
7757
  }
7624
7758
  else {
@@ -7653,7 +7787,7 @@ export class Grid extends GridBase {
7653
7787
  this.Repaint(false, true); // repaint full tiles
7654
7788
  }
7655
7789
 
7656
- this.layout.UpdateAnnotation(this.active_sheet.annotations);
7790
+ this.layout.UpdateAnnotation(this.active_sheet.annotations, this.theme);
7657
7791
  this.RenderSelections();
7658
7792
 
7659
7793
  }
@@ -89,7 +89,7 @@ export interface StructureEvent {
89
89
  export interface AnnotationEvent {
90
90
  type: 'annotation';
91
91
  annotation?: Annotation;
92
- event?: 'move'|'resize'|'create'|'delete'|'update';
92
+ event?: 'move'|'resize'|'create'|'delete'|'update'|'select';
93
93
  }
94
94
 
95
95
  export interface HyperlinkCellEventData {
@@ -90,6 +90,9 @@ export interface GridOptions {
90
90
  /** support MD formatting in text */
91
91
  markdown?: boolean;
92
92
 
93
+ /** support font stacks */
94
+ support_font_stacks?: boolean;
95
+
93
96
  }
94
97
 
95
98
  export const DefaultGridOptions: GridOptions = {
@@ -0,0 +1,134 @@
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
+ /*
23
+ * as of 2024 it looks like we can use proper fontmetrics in all browsers,
24
+ * so we'll switch to that. this is a replacement for the old fontmetrics
25
+ * (which read pixles), and any other font measurement utils.
26
+ *
27
+ * as far as I can tell the alphabatic baseline is constant, and reliable,
28
+ * in all browsers. so let's use that. other baselines seem to be slightly
29
+ * different, at least in firefox.
30
+ */
31
+
32
+ export interface FontMetrics {
33
+
34
+ /** from textmetrics, this is the font ascent (max, essentially) */
35
+ ascent: number;
36
+
37
+ /** from textmetrics, this is the font descent (max) */
38
+ descent: number;
39
+
40
+ /** total height for the font (line height). just ascent + descent. should we +1 for baseline? */
41
+ height: number;
42
+
43
+ /** width of one paren */
44
+ paren: number;
45
+
46
+ /** width of one hash (#) character */
47
+ hash: number;
48
+
49
+ }
50
+
51
+ // these two will be engine global, which is what we want
52
+ const cache: Map<string, FontMetrics> = new Map();
53
+ let canvas: HTMLCanvasElement | undefined;
54
+
55
+ /**
56
+ * get font metrics for the given font, which includes a size.
57
+ * precompute the size, we're not doing that anymore.
58
+ */
59
+ export const Get = (font: string, variants?: string) => {
60
+
61
+ const key = font;
62
+ // console.info({key});
63
+
64
+ let metrics = cache.get(key);
65
+
66
+ if (metrics) {
67
+ return metrics;
68
+ }
69
+
70
+ metrics = Measure(font, variants);
71
+ cache.set(key, metrics);
72
+ return metrics;
73
+
74
+ };
75
+
76
+ /**
77
+ * flush cache. this should be called when you update the theme
78
+ */
79
+ export const Flush = () => {
80
+ cache.clear();
81
+ };
82
+
83
+ /**
84
+ * do the actual measurement
85
+ */
86
+ const Measure = (font: string, variants?: string): FontMetrics => {
87
+
88
+ if (!canvas) {
89
+ if (typeof document !== 'undefined') {
90
+ canvas = document.createElement('canvas');
91
+ }
92
+ }
93
+
94
+ if (canvas) {
95
+ if (variants) {
96
+ canvas.style.fontVariant = variants;
97
+ }
98
+ else {
99
+ canvas.style.fontVariant = '';
100
+ }
101
+ }
102
+
103
+ const context = canvas?.getContext('2d', { alpha: false });
104
+ if (!context) {
105
+ throw new Error('invalid context');
106
+ }
107
+
108
+ context.textBaseline = 'alphabetic';
109
+ context.textAlign = 'center';
110
+ context.font = font;
111
+
112
+ let metrics = context.measureText('(');
113
+ const paren = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;
114
+
115
+ metrics = context.measureText('#');
116
+ const hash = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;
117
+
118
+ metrics = context.measureText('Mljy!');
119
+
120
+ return {
121
+
122
+ paren,
123
+ hash,
124
+
125
+ ascent: metrics.fontBoundingBoxAscent,
126
+ descent: metrics.fontBoundingBoxDescent,
127
+
128
+ height: (metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent),
129
+
130
+ };
131
+
132
+ };
133
+
134
+
@@ -209,8 +209,9 @@ export class Parser {
209
209
 
210
210
  /**
211
211
  * FIXME: why is this a class member? at a minimum it could be static
212
+ * FIXME: why are we doing this with a regex?
212
213
  */
213
- protected r1c1_regex = /[rR]((?:\[[-+]{0,1}\d+\]|\d+))[cC]((?:\[[-+]{0,1}\d+\]|\d+))$/;
214
+ protected r1c1_regex = /[rR]((?:\[[-+]{0,1}\d+\]|\d*))[cC]((?:\[[-+]{0,1}\d+\]|\d*))$/;
214
215
 
215
216
  /**
216
217
  * internal argument separator, as a number. this is set internally on
@@ -2556,17 +2557,25 @@ export class Parser {
2556
2557
  r1c1.offset_row = true;
2557
2558
  r1c1.row = Number(match[1].substring(1, match[1].length - 1));
2558
2559
  }
2559
- else { // absolute
2560
+ else if (match[1]){ // absolute
2560
2561
  r1c1.row = Number(match[1]) - 1; // R1C1 is 1-based
2561
2562
  }
2563
+ else {
2564
+ r1c1.offset_row = true;
2565
+ r1c1.row = 0;
2566
+ }
2562
2567
 
2563
2568
  if (match[2][0] === '[') { // relative
2564
2569
  r1c1.offset_column = true;
2565
2570
  r1c1.column = Number(match[2].substring(1, match[2].length - 1));
2566
2571
  }
2567
- else { // absolute
2572
+ else if (match[2]) { // absolute
2568
2573
  r1c1.column = Number(match[2]) - 1; // R1C1 is 1-based
2569
2574
  }
2575
+ else {
2576
+ r1c1.offset_column = true;
2577
+ r1c1.column = 0;
2578
+ }
2570
2579
 
2571
2580
  return r1c1;
2572
2581
 
@@ -120,7 +120,7 @@ export class Measurement {
120
120
 
121
121
  }
122
122
 
123
- /* *
123
+ /**
124
124
  * check if font is loaded, based on the theory that the alternatives
125
125
  * will be different sizes. note that this probably doesn't test weights
126
126
  * or italics properly, as those can be emulated without the specific face.
@@ -131,7 +131,7 @@ export class Measurement {
131
131
  * @param font_face
132
132
  * @param italic
133
133
  * @param bold
134
- * /
134
+ */
135
135
  public static FontLoaded(font_face: string, italic = false, weight = 400): boolean {
136
136
  const face = `${italic ? 'italic' : ''} ${weight} 20pt ${font_face}`;
137
137
  const m1 = this.MeasureText(`${face}, sans-serif`, `check font`);
@@ -139,7 +139,6 @@ export class Measurement {
139
139
  const m3 = this.MeasureText(`${face}, monospace`, `check font`);
140
140
  return (m1.width === m2.width && m2.width === m3.width);
141
141
  }
142
- */
143
142
 
144
143
  /**
145
144
  * measure width, height of text, accounting for rotation