@trebco/treb 28.10.5 → 28.13.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.
@@ -714,6 +714,9 @@ export class Exporter {
714
714
  /** overload for return type */
715
715
  public NormalizeAddress(unit: UnitRange, sheet: SerializedSheet): UnitRange;
716
716
 
717
+ /** extra overload */
718
+ public NormalizeAddress<UNIT = UnitAddress|UnitRange>(unit: UNIT, sheet: SerializedSheet): UNIT;
719
+
717
720
  /**
718
721
  * for charts we need addresses to be absolute ($) and ensure there's
719
722
  * a sheet name -- use the active sheet if it's not explicitly referenced
@@ -738,6 +741,19 @@ export class Exporter {
738
741
 
739
742
  }
740
743
 
744
+ public EnsureRange(unit: UnitAddress|UnitRange): UnitRange {
745
+ if (unit.type === 'range') {
746
+ return unit;
747
+ }
748
+ return {
749
+ type: 'range',
750
+ start: unit,
751
+ end: unit,
752
+ label: unit.label,
753
+ id: unit.id,
754
+ position: unit.position,
755
+ };
756
+ }
741
757
 
742
758
  /**
743
759
  * new-style annotation layout (kind of a two-cell anchor) to two-cell anchor
@@ -909,10 +925,13 @@ export class Exporter {
909
925
  }
910
926
  else if (/series/i.test(arg.name)) {
911
927
 
912
- const [label, x, y] = arg.args; // y is required
928
+ const [label, x, y, z] = arg.args; // y is required
913
929
 
914
- if (y && y.type === 'range') {
915
- options.data.push(this.NormalizeAddress(y, sheet_source));
930
+ // FIXME: could be address also [x, y]
931
+
932
+ if (y && (y.type === 'range' || y.type === 'address')) {
933
+
934
+ options.data.push(this.EnsureRange(this.NormalizeAddress(y, sheet_source)));
916
935
 
917
936
  if (label) {
918
937
 
@@ -933,9 +952,16 @@ export class Exporter {
933
952
  }
934
953
 
935
954
  if (!options.labels2) { options.labels2 = []; }
936
- if (x && x.type === 'range') {
937
- options.labels2[options.data.length - 1] = this.NormalizeAddress(x, sheet_source);
955
+
956
+ if (x && (x.type === 'range' || x.type === 'address')) {
957
+ options.labels2[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(x, sheet_source));
958
+ }
959
+
960
+ if (z && (z.type === 'range' || z.type === 'address')) {
961
+ if (!options.labels3) { options.labels3 = []; }
962
+ options.labels3[options.data.length - 1] = this.EnsureRange(this.NormalizeAddress(z, sheet_source));
938
963
  }
964
+
939
965
  }
940
966
  else {
941
967
  console.info('invalid series missing Y', {y, arg, ref});
@@ -951,30 +977,39 @@ export class Exporter {
951
977
  const parse_result = this.parser.Parse(annotation.formula || '');
952
978
  if (parse_result.expression && parse_result.expression.type === 'call') {
953
979
 
954
- let type = '';
980
+ let type = ''; // FIXME
981
+
955
982
  switch (parse_result.expression.name.toLowerCase()) {
956
983
  case 'line.chart':
957
984
  type = 'scatter';
958
985
  break;
986
+
987
+ case 'bubble.chart':
988
+ type = 'bubble';
989
+ break;
990
+
959
991
  case 'scatter.line':
960
992
  type = 'scatter2';
961
993
  break;
994
+
962
995
  case 'donut.chart':
963
996
  type = 'donut';
964
997
  break;
998
+
965
999
  case 'bar.chart':
966
1000
  type = 'bar';
967
1001
  break;
1002
+
968
1003
  case 'column.chart':
969
1004
  type = 'column';
970
1005
  break;
971
1006
  }
972
1007
 
973
- if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2') {
1008
+ if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2' || type === 'bubble') {
974
1009
 
975
1010
  const options: ChartOptions = { type, data: [] };
976
1011
 
977
- const title_index = (type === 'scatter2') ? 1 : 2;
1012
+ const title_index = (type === 'scatter2' || type === 'bubble') ? 1 : 2;
978
1013
  const title_arg = parse_result.expression.args[title_index];
979
1014
 
980
1015
  if (title_arg && title_arg.type === 'literal') {
@@ -1000,7 +1035,7 @@ export class Exporter {
1000
1035
 
1001
1036
  if (parse_result.expression.args[0]) {
1002
1037
  const arg0 = parse_result.expression.args[0];
1003
- if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter') {
1038
+ if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter' || type === 'bubble') {
1004
1039
  parse_series(arg0, options, sheet_source.name);
1005
1040
  }
1006
1041
  else if (arg0.type === 'range') {
@@ -1045,7 +1080,7 @@ export class Exporter {
1045
1080
  */
1046
1081
  }
1047
1082
 
1048
- if (type !== 'scatter2') {
1083
+ if (type !== 'scatter2' && type !== 'bubble') {
1049
1084
  if (parse_result.expression.args[1] && parse_result.expression.args[1].type === 'range') {
1050
1085
  options.labels = this.NormalizeAddress(parse_result.expression.args[1], sheet_source);
1051
1086
  }
@@ -1064,6 +1099,10 @@ export class Exporter {
1064
1099
  options.smooth = true;
1065
1100
  }
1066
1101
  }
1102
+ else if (type === 'bubble') {
1103
+ // ...
1104
+ // console.info({parse_result});
1105
+ }
1067
1106
 
1068
1107
  // FIXME: fix this type (this happened when we switched from annotation
1069
1108
  // class to a data interface)
@@ -81,7 +81,10 @@ export class Importer {
81
81
  t?: string;
82
82
  s?: string;
83
83
  };
84
- v?: string|number; // is this never an object? (note: booleans are numbers in Excel)
84
+ v?: string|number|{
85
+ t$: string;
86
+ a$?: any;
87
+ };
85
88
  f?: string|{
86
89
  t$: string;
87
90
  a$?: {
@@ -214,14 +217,17 @@ export class Importer {
214
217
  }
215
218
 
216
219
  if (typeof element.v !== 'undefined') {
217
- const num = Number(element.v.toString());
220
+
221
+ const V = (typeof element.v === 'object') ? element.v?.t$ : element.v;
222
+
223
+ const num = Number(V.toString());
218
224
  if (!isNaN(num)) {
219
225
  calculated_type = 'number'; // ValueType.number;
220
226
  calculated_value = num;
221
227
  }
222
228
  else {
223
229
  calculated_type = 'string'; // ValueType.string;
224
- calculated_value = element.v.toString();
230
+ calculated_value = V.toString();
225
231
  }
226
232
  }
227
233
 
@@ -917,6 +923,27 @@ export class Importer {
917
923
  const series = descriptor.chart?.series;
918
924
 
919
925
  switch(descriptor.chart.type) {
926
+
927
+ case ChartType.Bubble:
928
+ type = 'treb-chart';
929
+ func = 'Bubble.Chart';
930
+
931
+ if (series && series.length) {
932
+ args[0] = `Group(${series.map(s => `Series(${
933
+ [
934
+ s.title || '',
935
+ s.values || '',
936
+ s.categories || '',
937
+ s.bubble_size || '',
938
+
939
+ ].join(', ')
940
+ })`).join(', ')})`;
941
+ }
942
+
943
+ args[1] = descriptor.chart.title;
944
+
945
+ break;
946
+
920
947
  case ChartType.Scatter:
921
948
  type = 'treb-chart';
922
949
  func = 'Scatter.Line';
@@ -29,6 +29,9 @@ import { XMLUtils } from './xml-utils';
29
29
 
30
30
  import { Unescape } from './unescape_xml';
31
31
 
32
+ // what's the default font size? ... 11pt?
33
+ const DEFAULT_FONT_SIZE = 11;
34
+
32
35
  export interface Font {
33
36
  size?: number;
34
37
  name?: string;
@@ -269,13 +272,24 @@ export class StyleCache {
269
272
 
270
273
  }
271
274
 
275
+
272
276
  if (composite.font_size?.unit && composite.font_size.value) {
273
- if (composite.font_size.unit !== 'pt') {
274
- console.warn(`can't handle non-point font (FIXME)`);
277
+ if (composite.font_size.unit === 'em') {
278
+ font.size = composite.font_size.value * DEFAULT_FONT_SIZE;
275
279
  }
276
- else {
280
+ else if (composite.font_size.unit === '%') {
281
+ font.size = composite.font_size.value * DEFAULT_FONT_SIZE / 100;
282
+ }
283
+ else if (composite.font_size.unit === 'pt' ){
277
284
  font.size = composite.font_size.value;
278
285
  }
286
+ else if (composite.font_size.unit === 'px' ){
287
+ font.size = composite.font_size.value * .75; // ?
288
+ }
289
+ else {
290
+ console.warn(`Unhandled font size unit`, composite.font_size);
291
+ }
292
+
279
293
  }
280
294
 
281
295
  if (composite.bold) font.bold = true;
@@ -657,6 +671,19 @@ export class StyleCache {
657
671
 
658
672
  // borders
659
673
 
674
+ const BorderEdgeToColor = (edge: BorderEdge): Color|undefined => {
675
+
676
+ // TODO: indexed
677
+
678
+ if (typeof edge.theme !== 'undefined') {
679
+ return {
680
+ theme: edge.theme,
681
+ tint: edge.tint,
682
+ };
683
+ }
684
+
685
+ };
686
+
660
687
  const border = this.borders[xf.border || 0];
661
688
  if (border) {
662
689
  if (border.bottom.style) {
@@ -666,10 +693,20 @@ export class StyleCache {
666
693
  else {
667
694
  props.border_bottom = 1;
668
695
  }
696
+ props.border_bottom_fill = BorderEdgeToColor(border.bottom);
697
+ }
698
+ if (border.left.style) {
699
+ props.border_left = 1;
700
+ props.border_left_fill = BorderEdgeToColor(border.left);
701
+ }
702
+ if (border.top.style) {
703
+ props.border_top = 1;
704
+ props.border_top_fill = BorderEdgeToColor(border.top);
705
+ }
706
+ if (border.right.style) {
707
+ props.border_right = 1;
708
+ props.border_right_fill = BorderEdgeToColor(border.right);
669
709
  }
670
- if (border.left.style) props.border_left = 1;
671
- if (border.top.style) props.border_top = 1;
672
- if (border.right.style) props.border_right = 1;
673
710
  }
674
711
 
675
712
  return props;
@@ -1144,31 +1181,58 @@ export class StyleCache {
1144
1181
 
1145
1182
  composite = FindAll('styleSheet/borders/border');
1146
1183
 
1184
+ const ElementToBorderEdge = (element: any, edge: BorderEdge) => {
1185
+
1186
+ if (element?.a$) {
1187
+ edge.style = element.a$.style;
1188
+ if (typeof element.color === 'object') {
1189
+ if (typeof element.color.a$?.indexed !== 'undefined') {
1190
+ edge.color = Number(element.color.a$.indexed);
1191
+ }
1192
+ if (typeof element.color.a$?.theme !== 'undefined') {
1193
+ edge.theme = Number(element.color.a$.theme);
1194
+ }
1195
+ if (typeof element.color.a$?.tint !== 'undefined') {
1196
+ edge.tint = Number(element.color.a$.tint);
1197
+ }
1198
+ }
1199
+ }
1200
+
1201
+ };
1202
+
1203
+
1147
1204
  this.borders = composite.map(element => {
1148
1205
 
1149
1206
  const border: BorderStyle = JSON.parse(JSON.stringify(default_border));
1150
1207
 
1208
+ /*
1151
1209
  // we're relying on these being empty strings -> falsy, not a good look
1152
1210
 
1153
1211
  if (element.left) {
1154
- border.left.style = element.left.a$.style;
1155
- border.left.color = Number(element.left.color?.a$?.indexed);
1212
+ // border.left.style = element.left.a$.style;
1213
+ // border.left.color = Number(element.left.color?.a$?.indexed);
1156
1214
  }
1157
1215
 
1158
1216
  if (element.right) {
1159
- border.right.style = element.right.a$.style;
1160
- border.right.color = Number(element.right.color?.a$?.indexed);
1217
+ // border.right.style = element.right.a$.style;
1218
+ // border.right.color = Number(element.right.color?.a$?.indexed);
1161
1219
  }
1162
1220
 
1163
1221
  if (element.top) {
1164
- border.top.style = element.top.a$.style;
1165
- border.top.color = Number(element.top.color?.a$?.indexed);
1222
+ // border.top.style = element.top.a$.style;
1223
+ // border.top.color = Number(element.top.color?.a$?.indexed);
1166
1224
  }
1167
1225
 
1168
1226
  if (element.bottom) {
1169
- border.bottom.style = element.bottom.a$.style;
1170
- border.bottom.color = Number(element.bottom.color?.a$?.indexed);
1227
+ // border.bottom.style = element.bottom.a$.style;
1228
+ // border.bottom.color = Number(element.bottom.color?.a$?.indexed);
1171
1229
  }
1230
+ */
1231
+
1232
+ ElementToBorderEdge(element.left, border.left);
1233
+ ElementToBorderEdge(element.right, border.right);
1234
+ ElementToBorderEdge(element.top, border.top);
1235
+ ElementToBorderEdge(element.bottom, border.bottom);
1172
1236
 
1173
1237
  return border;
1174
1238
 
@@ -69,12 +69,13 @@ export const ConditionalFormatOperators: Record<string, string> = {
69
69
  };
70
70
 
71
71
  export enum ChartType {
72
- Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie
72
+ Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble
73
73
  }
74
74
 
75
75
  export interface ChartSeries {
76
76
  values?: string;
77
77
  categories?: string;
78
+ bubble_size?: string;
78
79
  title?: string;
79
80
  }
80
81
 
@@ -411,7 +412,7 @@ export class Workbook {
411
412
 
412
413
  }
413
414
 
414
- const ParseSeries = (node: any, scatter = false): ChartSeries[] => {
415
+ const ParseSeries = (node: any, type?: ChartType): ChartSeries[] => {
415
416
 
416
417
  const series: ChartSeries[] = [];
417
418
 
@@ -447,7 +448,7 @@ export class Workbook {
447
448
  }
448
449
  }
449
450
 
450
- if (scatter) {
451
+ if (type === ChartType.Scatter || type === ChartType.Bubble) {
451
452
  const x = XMLUtils.FindChild(series_node, 'c:xVal/c:numRef/c:f');
452
453
  if (x) {
453
454
  series_data.categories = x; // .text?.toString();
@@ -456,6 +457,14 @@ export class Workbook {
456
457
  if (y) {
457
458
  series_data.values = y; // .text?.toString();
458
459
  }
460
+
461
+ if (type === ChartType.Bubble) {
462
+ const z = XMLUtils.FindChild(series_node, 'c:bubbleSize/c:numRef/c:f');
463
+ if (z) {
464
+ series_data.bubble_size = z; // .text?.toString();
465
+ }
466
+ }
467
+
459
468
  }
460
469
  else {
461
470
  const value_node = XMLUtils.FindChild(series_node, 'c:val/c:numRef/c:f');
@@ -522,9 +531,23 @@ export class Workbook {
522
531
  node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:scatterChart');
523
532
  if (node) {
524
533
  result.type = ChartType.Scatter;
525
- result.series = ParseSeries(node, true);
534
+ result.series = ParseSeries(node, ChartType.Scatter);
526
535
  }
527
536
  }
537
+
538
+ if (!node) {
539
+ node = XMLUtils.FindChild(xml, 'c:chartSpace/c:chart/c:plotArea/c:bubbleChart');
540
+ if (node) {
541
+ result.type = ChartType.Bubble;
542
+ result.series = ParseSeries(node, ChartType.Bubble);
543
+ console.info("Bubble series?", result.series);
544
+ }
545
+ }
546
+
547
+ if (!node) {
548
+ console.info("Chart type not handled");
549
+ }
550
+
528
551
  // console.info("RX?", result);
529
552
 
530
553
  return result;
@@ -789,6 +789,13 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
789
789
 
790
790
  if (this.active_editor && !this.assume_formula) {
791
791
  this.active_editor.node.spellcheck = !(this.text_formula);
792
+
793
+ // if not assuming formula, and it's not a formula, then exit.
794
+
795
+ if (!this.text_formula) {
796
+ return;
797
+ }
798
+
792
799
  }
793
800
 
794
801
  // this is a short-circuit so we don't format the same text twice.
@@ -835,9 +835,9 @@ export class Grid extends GridBase {
835
835
  }
836
836
  }
837
837
 
838
- /**
838
+ /* *
839
839
  * specialization: update selection, scroll offset
840
- */
840
+ * /
841
841
  public Serialize(options: SerializeOptions = {}): SerializedModel {
842
842
 
843
843
  // selection moved to sheet, but it's not "live"; so we need to
@@ -855,6 +855,7 @@ export class Grid extends GridBase {
855
855
  return super.Serialize(options);
856
856
 
857
857
  }
858
+ */
858
859
 
859
860
  /**
860
861
  * show or hide headers
@@ -1830,18 +1831,23 @@ export class Grid extends GridBase {
1830
1831
  argument_separator: this.parser.argument_separator,
1831
1832
  decimal_mark: this.parser.decimal_mark,
1832
1833
  }
1834
+
1835
+ this.parser.Save();
1836
+
1833
1837
  let convert = false;
1834
1838
 
1835
1839
  if (options.argument_separator === ',' && this.parser.argument_separator !== ArgumentSeparatorType.Comma) {
1836
- this.parser.argument_separator = ArgumentSeparatorType.Comma;
1837
- this.parser.decimal_mark = DecimalMarkType.Period;
1840
+ // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1841
+ // this.parser.decimal_mark = DecimalMarkType.Period;
1842
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
1838
1843
 
1839
1844
  convert = true;
1840
1845
  }
1841
1846
 
1842
1847
  if (options.argument_separator === ';' && this.parser.argument_separator !== ArgumentSeparatorType.Semicolon) {
1843
- this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1844
- this.parser.decimal_mark = DecimalMarkType.Comma;
1848
+ // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1849
+ // this.parser.decimal_mark = DecimalMarkType.Comma;
1850
+ this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1845
1851
 
1846
1852
  convert = true;
1847
1853
  }
@@ -1882,8 +1888,10 @@ export class Grid extends GridBase {
1882
1888
 
1883
1889
  // reset
1884
1890
 
1885
- this.parser.argument_separator = current.argument_separator;
1886
- this.parser.decimal_mark = current.decimal_mark;
1891
+ // this.parser.argument_separator = current.argument_separator;
1892
+ // this.parser.decimal_mark = current.decimal_mark;
1893
+
1894
+ this.parser.Restore();
1887
1895
 
1888
1896
  }
1889
1897
 
@@ -4439,7 +4447,10 @@ export class Grid extends GridBase {
4439
4447
 
4440
4448
  if (this.overlay_editor?.selection) {
4441
4449
  const value = this.overlay_editor?.edit_node.textContent || undefined;
4442
- const array = (event.key === 'Enter' && event.ctrlKey && event.shiftKey);
4450
+
4451
+ // let's support command+shift+enter on mac
4452
+ const array = (event.key === 'Enter' && (event.ctrlKey || (UA.is_mac && event.metaKey)) && event.shiftKey);
4453
+
4443
4454
  this.SetInferredType(this.overlay_editor.selection, value, array);
4444
4455
  }
4445
4456
 
@@ -461,13 +461,19 @@ export class GridBase {
461
461
  // CSV assumes dot-decimal, correct? if we want to use the
462
462
  // parser we will have to check (and set/reset) the separator
463
463
 
464
+ this.parser.Save();
465
+ this.parser.SetLocaleSettings(DecimalMarkType.Period);
466
+
467
+ /*
464
468
  const toggle_separator = this.parser.decimal_mark === DecimalMarkType.Comma;
465
469
 
466
- if (toggle_separator) {
470
+ if (toggle_separator)
471
+ {
467
472
  // swap
468
473
  this.parser.argument_separator = ArgumentSeparatorType.Comma;
469
474
  this.parser.decimal_mark = DecimalMarkType.Period;
470
475
  }
476
+ */
471
477
 
472
478
  const records = ParseCSV(text);
473
479
  const arr = records.map((record) =>
@@ -481,11 +487,14 @@ export class GridBase {
481
487
  return ValueParser.TryParse(field).value;
482
488
  }));
483
489
 
490
+ /*
484
491
  if (toggle_separator) {
485
492
  // reset
486
493
  this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
487
494
  this.parser.decimal_mark = DecimalMarkType.Comma;
488
495
  }
496
+ */
497
+ this.parser.Restore();
489
498
 
490
499
  const end = {
491
500
  row: Math.max(0, arr.length - 1),
@@ -735,110 +744,6 @@ export class GridBase {
735
744
  this.model.user_data = undefined;
736
745
  }
737
746
 
738
- /**
739
- * serialize data. this function used to (optionally) stringify
740
- * by typescript has a problem figuring this out, so we will simplify
741
- * the function.
742
- */
743
- public Serialize(options: SerializeOptions = {}): SerializedModel {
744
-
745
- // (removed UI stuff, that goes in subclass)
746
-
747
- // selection moved to sheet, but it's not "live"; so we need to
748
- // capture the primary selection in the current active sheet before
749
- // we serialize it
750
-
751
- // this.active_sheet.selection = JSON.parse(JSON.stringify(this.primary_selection));
752
-
753
- // same for scroll offset
754
-
755
- // this.active_sheet.scroll_offset = this.layout.scroll_offset;
756
-
757
- // NOTE: annotations moved to sheets, they will be serialized in the sheets
758
-
759
- const sheet_data = this.model.sheets.list.map((sheet) => sheet.toJSON(options));
760
-
761
- // OK, not serializing tables in cells anymore. old comment about this:
762
- //
763
- // at the moment, tables are being serialized in cells. if we put them
764
- // in here, then we have two records of the same data. that would be bad.
765
- // I think this is probably the correct place, but if we put them here
766
- // we need to stop serializing in cells. and I'm not sure that there are
767
- // not some side-effects to that. hopefully not, but (...)
768
- //
769
-
770
- let tables: Table[] | undefined;
771
- if (this.model.tables.size > 0) {
772
- tables = Array.from(this.model.tables.values());
773
- }
774
-
775
- // NOTE: moving into a structured object (the sheet data is also structured,
776
- // of course) but we are moving things out of sheet (just named ranges atm))
777
-
778
- let macro_functions: MacroFunction[] | undefined;
779
-
780
- if (this.model.macro_functions.size) {
781
- macro_functions = [];
782
- for (const macro of this.model.macro_functions.values()) {
783
- macro_functions.push({
784
- ...macro,
785
- expression: undefined,
786
- });
787
- }
788
- }
789
-
790
- // when serializing named expressions, we have to make sure
791
- // that there's a sheet name in any address/range.
792
-
793
- const named_expressions: SerializedNamedExpression[] = [];
794
- if (this.model.named_expressions) {
795
-
796
- for (const [name, expr] of this.model.named_expressions) {
797
- this.parser.Walk(expr, unit => {
798
- if (unit.type === 'address' || unit.type === 'range') {
799
- const test = unit.type === 'range' ? unit.start : unit;
800
-
801
- test.absolute_column = test.absolute_row = true;
802
-
803
- if (!test.sheet) {
804
- if (test.sheet_id) {
805
- const sheet = this.model.sheets.Find(test.sheet_id);
806
- if (sheet) {
807
- test.sheet = sheet.name;
808
- }
809
- }
810
- if (!test.sheet) {
811
- test.sheet = this.active_sheet.name;
812
- }
813
- }
814
-
815
- if (unit.type === 'range') {
816
- unit.end.absolute_column = unit.end.absolute_row = true;
817
- }
818
-
819
- return false;
820
- }
821
- return true;
822
- });
823
- const rendered = this.parser.Render(expr, { missing: '' });
824
- named_expressions.push({
825
- name, expression: rendered
826
- });
827
- }
828
- }
829
-
830
- return {
831
- sheet_data,
832
- active_sheet: this.active_sheet.id,
833
- named_ranges: this.model.named_ranges.Count() ?
834
- this.model.named_ranges.Serialize() :
835
- undefined,
836
- macro_functions,
837
- tables,
838
- named_expressions: named_expressions.length ? named_expressions : undefined,
839
- };
840
-
841
- }
842
747
 
843
748
  // --- protected methods -----------------------------------------------------
844
749
 
@@ -69,4 +69,9 @@ export interface SerializeOptions {
69
69
  /** share resources (images, for now) to prevent writing data URIs more than once */
70
70
  share_resources?: boolean;
71
71
 
72
+ /**
73
+ * if a function has an export() handler, call that
74
+ */
75
+ export_functions?: boolean;
76
+
72
77
  }