@trebco/treb 29.8.3 → 30.1.0

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 (38) hide show
  1. package/dist/treb-spreadsheet-light.mjs +11 -11
  2. package/dist/treb-spreadsheet.mjs +15 -15
  3. package/dist/treb.d.ts +11 -1
  4. package/eslint.config.js +9 -0
  5. package/package.json +1 -1
  6. package/treb-base-types/src/area-utils.ts +60 -0
  7. package/treb-base-types/src/area.ts +11 -0
  8. package/treb-base-types/src/cell.ts +6 -1
  9. package/treb-base-types/src/cells.ts +38 -7
  10. package/treb-base-types/src/index.ts +2 -0
  11. package/treb-calculator/src/calculator.ts +274 -4
  12. package/treb-calculator/src/dag/array-vertex.ts +0 -10
  13. package/treb-calculator/src/dag/graph.ts +118 -77
  14. package/treb-calculator/src/dag/spreadsheet_vertex.ts +38 -9
  15. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +1 -0
  16. package/treb-calculator/src/expression-calculator.ts +7 -2
  17. package/treb-calculator/src/function-error.ts +6 -0
  18. package/treb-charts/src/chart-functions.ts +39 -5
  19. package/treb-charts/src/chart-types.ts +23 -0
  20. package/treb-charts/src/chart-utils.ts +165 -2
  21. package/treb-charts/src/chart.ts +6 -1
  22. package/treb-charts/src/default-chart-renderer.ts +70 -1
  23. package/treb-charts/src/index.ts +1 -0
  24. package/treb-charts/src/renderer.ts +95 -2
  25. package/treb-charts/style/charts.scss +41 -0
  26. package/treb-embed/src/embedded-spreadsheet.ts +11 -4
  27. package/treb-embed/src/options.ts +8 -0
  28. package/treb-embed/style/dark-theme.scss +4 -0
  29. package/treb-embed/style/grid.scss +15 -0
  30. package/treb-embed/style/z-index.scss +3 -0
  31. package/treb-export/src/import2.ts +9 -0
  32. package/treb-export/src/workbook2.ts +67 -3
  33. package/treb-grid/src/editors/editor.ts +12 -5
  34. package/treb-grid/src/layout/base_layout.ts +41 -0
  35. package/treb-grid/src/types/grid.ts +72 -28
  36. package/treb-parser/src/parser-types.ts +3 -0
  37. package/treb-parser/src/parser.ts +21 -2
  38. package/treb-utils/src/serialize_html.ts +35 -10
@@ -49,7 +49,7 @@ export const ConditionalFormatOperators: Record<string, string> = {
49
49
  };
50
50
 
51
51
  export enum ChartType {
52
- Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble
52
+ Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble, Box
53
53
  }
54
54
 
55
55
  export interface ChartSeries {
@@ -345,7 +345,14 @@ export class Workbook {
345
345
  to: ParseAnchor(anchor_node['xdr:to']),
346
346
  };
347
347
 
348
- const chart_reference = XMLUtils.FindAll(anchor_node, `xdr:graphicFrame/a:graphic/a:graphicData/c:chart`)[0];
348
+ let chart_reference = XMLUtils.FindAll(anchor_node, `xdr:graphicFrame/a:graphic/a:graphicData/c:chart`)[0];
349
+
350
+ // check for an "alternate content" chart/chartex (wtf ms). we're
351
+ // supporting this for box charts only (atm)
352
+
353
+ if (!chart_reference) {
354
+ chart_reference = XMLUtils.FindAll(anchor_node, `mc:AlternateContent/mc:Choice/xdr:graphicFrame/a:graphic/a:graphicData/cx:chart`)[0];
355
+ }
349
356
 
350
357
  if (chart_reference && chart_reference.a$ && chart_reference.a$['r:id']) {
351
358
  const result: AnchoredChartDescription = { type: 'chart', anchor };
@@ -659,7 +666,64 @@ export class Workbook {
659
666
  if (node) {
660
667
  result.type = ChartType.Bubble;
661
668
  result.series = ParseSeries(node, ChartType.Bubble);
662
- console.info("Bubble series?", result.series);
669
+ // console.info("Bubble series?", result.series);
670
+ }
671
+ }
672
+
673
+ if (!node) {
674
+
675
+ // box plot uses "extended chart" which is totally different... but we
676
+ // might need it again later? for the time being it's just inlined
677
+
678
+ const ex_series = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:plotArea/cx:plotAreaRegion/cx:series');
679
+ if (ex_series?.length) {
680
+ if (ex_series.every(test => test.__layoutId === 'boxWhisker')) {
681
+ result.type = ChartType.Box;
682
+ result.series = [];
683
+ const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data'); // /cx:data/cx:numDim/cx:f');
684
+
685
+ // console.info({ex_series, data})
686
+
687
+ for (const entry of ex_series) {
688
+
689
+ const series: ChartSeries = {};
690
+
691
+ const id = Number(entry['cx:dataId']?.['__val']);
692
+ for (const data_series of data) {
693
+ if (Number(data_series.__id) === id) {
694
+ series.values = data_series['cx:numDim']?.['cx:f'] || '';
695
+ break;
696
+ }
697
+ }
698
+
699
+ const label = XMLUtils.FindAll(entry, 'cx:tx/cx:txData');
700
+ if (label) {
701
+ if (label[0]?.['cx:f']) {
702
+ series.title = label[0]['cx:f'];
703
+ }
704
+ else if (label[0]?.['cx:v']) {
705
+ series.title = '"' + label[0]['cx:v'] + '"';
706
+ }
707
+ }
708
+
709
+ const title = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:title/cx:tx/cx:txData');
710
+ if (title) {
711
+ if (title[0]?.['cx:f']) {
712
+ result.title = title[0]['cx:f'];
713
+ }
714
+ else if (title[0]?.['cx:v']) {
715
+ result.title = '"' + title[0]['cx:v'] + '"';
716
+ }
717
+ }
718
+
719
+ result.series.push(series);
720
+
721
+ }
722
+
723
+ // console.info({result});
724
+ return result;
725
+
726
+ }
663
727
  }
664
728
  }
665
729
 
@@ -726,7 +726,8 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
726
726
  const list: Set<string> = new Set();
727
727
 
728
728
  // for the result, map of reference to normalized address label
729
- const map: Map<ExpressionUnit, string> = new Map();
729
+ // const map: Map<ExpressionUnit, string> = new Map();
730
+ const map: Map<string, string> = new Map();
730
731
 
731
732
  for (const entry of reference_list) {
732
733
 
@@ -743,12 +744,14 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
743
744
  }
744
745
 
745
746
  // but keep a map
746
- map.set(entry, label);
747
+ map.set(entry.label, label);
747
748
 
748
749
  }
749
750
 
750
751
  this.UpdateReferences(descriptor, references);
751
752
 
753
+ // console.info({map});
754
+
752
755
  return map;
753
756
 
754
757
  }
@@ -851,7 +854,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
851
854
  if (parse_result.expression) {
852
855
 
853
856
  const normalized_labels = this.UpdateDependencies(descriptor, parse_result);
854
-
857
+
855
858
  // the parser will drop a leading = character, so be
856
859
  // sure to add that back if necessary
857
860
 
@@ -925,8 +928,12 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
925
928
 
926
929
  switch (unit.type) {
927
930
  case 'identifier':
928
- case 'call':
931
+ // FIXME: canonicalize (optionally)
932
+ label = text.substring(pos, pos + unit.name.length);
933
+ reference = normalized_labels.get(unit.name) || '';
934
+ break;
929
935
 
936
+ case 'call':
930
937
  // FIXME: canonicalize (optionally)
931
938
  label = text.substring(pos, pos + unit.name.length);
932
939
  break;
@@ -944,7 +951,7 @@ export class Editor<E = FormulaEditorEvent> extends EventSource<E|FormulaEditorE
944
951
  case 'address':
945
952
  case 'range':
946
953
  case 'structured-reference':
947
- reference = normalized_labels.get(unit) || '???';
954
+ reference = normalized_labels.get(unit.label) || '???';
948
955
 
949
956
  /*
950
957
  {
@@ -149,6 +149,7 @@ export abstract class BaseLayout {
149
149
  }
150
150
 
151
151
  protected dropdown_caret!: SVGSVGElement;
152
+ protected spill_border!: SVGSVGElement;
152
153
 
153
154
  /** we have to disable mock selection for IE or it breaks key handling */
154
155
  private trident = ((typeof navigator !== 'undefined') &&
@@ -207,6 +208,9 @@ export abstract class BaseLayout {
207
208
  this.mask = DOM.Div('treb-mouse-mask');
208
209
  this.tooltip = DOM.Div('treb-tooltip');
209
210
 
211
+ this.spill_border = DOM.SVG('svg', 'treb-spill-border');
212
+ this.spill_border.tabIndex = -1;
213
+
210
214
  this.dropdown_caret = DOM.SVG('svg', 'treb-dropdown-caret');
211
215
  this.dropdown_caret.setAttribute('viewBox', '0 0 24 24');
212
216
  this.dropdown_caret.tabIndex = -1;
@@ -1099,6 +1103,10 @@ export abstract class BaseLayout {
1099
1103
 
1100
1104
  // FIXME: -> instance specific, b/c trident
1101
1105
 
1106
+ if (!this.spill_border.parentElement) {
1107
+ container.appendChild(this.spill_border);
1108
+ }
1109
+
1102
1110
  if (!this.dropdown_caret.parentElement) {
1103
1111
  container.appendChild(this.dropdown_caret);
1104
1112
  }
@@ -1431,6 +1439,39 @@ export abstract class BaseLayout {
1431
1439
  this.dropdown_caret_visible = true;
1432
1440
  }
1433
1441
 
1442
+ public ShowSpillBorder(area?: IArea) {
1443
+ this.spill_border.textContent = '';
1444
+ if (area) {
1445
+ const resolved = new Area(area.start, area.end);
1446
+
1447
+ let target_rect = this.OffsetCellAddressToRectangle(resolved.start);
1448
+
1449
+ if (resolved.count > 1) {
1450
+ target_rect = target_rect.Combine(this.OffsetCellAddressToRectangle(resolved.end));
1451
+ }
1452
+
1453
+ target_rect = target_rect.Shift(
1454
+ this.header_size.width, this.header_size.height);
1455
+
1456
+ this.spill_border.style.display = 'block';
1457
+ this.spill_border.style.top = (target_rect.top - 5).toString();
1458
+ this.spill_border.style.left = (target_rect.left - 5).toString();
1459
+ this.spill_border.style.width = (target_rect.width + 10).toString();
1460
+ this.spill_border.style.height = (target_rect.height + 10).toString();
1461
+
1462
+ const rect = this.DOM.SVG('rect', undefined, this.spill_border);
1463
+ rect.setAttribute('x', '4.5');
1464
+ rect.setAttribute('y', '4.5');
1465
+ rect.setAttribute('width', (target_rect.width + 1).toString());
1466
+ rect.setAttribute('height', (target_rect.height + 1).toString());
1467
+
1468
+ }
1469
+ else {
1470
+ this.spill_border.style.display = 'none';
1471
+ }
1472
+
1473
+ }
1474
+
1434
1475
  public HideDropdownCaret(): void {
1435
1476
  if (this.dropdown_caret_visible) {
1436
1477
  // this.dropdown_caret.classList.remove('active');
@@ -48,7 +48,7 @@ import {
48
48
  IsArea,
49
49
  } from 'treb-base-types';
50
50
 
51
- import type { ExpressionUnit, UnitAddress } from 'treb-parser';
51
+ import type { ExpressionUnit, RenderOptions, UnitAddress } from 'treb-parser';
52
52
  import {
53
53
  DecimalMarkType,
54
54
  ArgumentSeparatorType,
@@ -311,6 +311,13 @@ export class Grid extends GridBase {
311
311
  empty: true,
312
312
  };
313
313
 
314
+ /** reusing type. FIXME? we don't need a target */
315
+ private readonly spill_selection: GridSelection = {
316
+ target: { row: 0, column: 0 },
317
+ area: new Area({ row: 0, column: 0 }),
318
+ empty: true,
319
+ };
320
+
314
321
  /**
315
322
  * active selection when selecting arguments (while editing)
316
323
  */
@@ -1353,6 +1360,7 @@ export class Grid extends GridBase {
1353
1360
  */
1354
1361
  public UpdateLayout(): void {
1355
1362
  this.layout.UpdateTiles();
1363
+ this.layout.UpdateContentsSize();
1356
1364
  this.render_tiles = this.layout.VisibleTiles();
1357
1365
  this.Repaint(true);
1358
1366
  }
@@ -4614,9 +4622,17 @@ export class Grid extends GridBase {
4614
4622
  }
4615
4623
 
4616
4624
  for (let row = start; row >= 0 && row < target_rows; row += step, offset += step, pattern_increment += pattern) {
4625
+
4626
+ const render_options: Partial<RenderOptions> = {
4627
+ offset: transposed ? {
4628
+ rows: 0, columns: offset,
4629
+ } : {
4630
+ rows: offset, columns: 0,
4631
+ }
4632
+ };
4633
+
4617
4634
  if (translate) {
4618
- data[row][column] = '=' + this.parser.Render(translate, {
4619
- offset: { rows: offset, columns: 0 }});
4635
+ data[row][column] = '=' + this.parser.Render(translate, render_options);
4620
4636
  }
4621
4637
  else {
4622
4638
  const cell = cells[source_row][column];
@@ -5091,13 +5107,16 @@ export class Grid extends GridBase {
5091
5107
 
5092
5108
  const cell = this.active_sheet.CellData(this.primary_selection.target);
5093
5109
 
5094
- if (!cell || (!cell.area && !cell.table)) {
5110
+ if (!cell || (!cell.area && !cell.table && !cell.spill)) {
5095
5111
  return;
5096
5112
  }
5097
5113
 
5098
5114
  if (cell.area) {
5099
5115
  this.Select(this.primary_selection, cell.area, cell.area.start);
5100
5116
  }
5117
+ if (cell.spill) {
5118
+ this.Select(this.primary_selection, cell.spill, cell.spill.start);
5119
+ }
5101
5120
  if (cell.table) {
5102
5121
  const area = new Area(cell.table.area.start, cell.table.area.end);
5103
5122
  this.Select(this.primary_selection, area, area.start);
@@ -5115,7 +5134,12 @@ export class Grid extends GridBase {
5115
5134
 
5116
5135
  const show_primary_selection = this.hide_selection ? false :
5117
5136
  (!this.editing_state) || (this.editing_cell.sheet_id === this.active_sheet.id);
5118
-
5137
+
5138
+ const data = this.primary_selection.empty ? undefined :
5139
+ this.active_sheet.CellData(this.primary_selection.target);
5140
+
5141
+ this.layout.ShowSpillBorder(data?.spill);
5142
+
5119
5143
  this.selection_renderer?.RenderSelections(show_primary_selection, rerender);
5120
5144
  }
5121
5145
 
@@ -5164,7 +5188,7 @@ export class Grid extends GridBase {
5164
5188
  const cells = this.active_sheet.cells;
5165
5189
 
5166
5190
  let cell = cells.GetCell(selection.target, false);
5167
- if (!cell || (cell.type === ValueType.undefined && !cell.area)) {
5191
+ if (!cell || (cell.type === ValueType.undefined && !cell.area && !cell.spill)) {
5168
5192
  return false;
5169
5193
  }
5170
5194
 
@@ -5198,20 +5222,20 @@ export class Grid extends GridBase {
5198
5222
  if (rows) {
5199
5223
  for (let column = selection.area.start.column; !has_value && column <= selection.area.end.column; column++) {
5200
5224
  cell = cells.GetCell({ row: test.row, column }, false);
5201
- has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area));
5225
+ has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area || !!cell.spill));
5202
5226
  if (!has_value && cell && cell.merge_area) {
5203
5227
  cell = cells.GetCell(cell.merge_area.start, false);
5204
- has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area));
5228
+ has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area || !!cell.spill));
5205
5229
  }
5206
5230
  }
5207
5231
  }
5208
5232
  else {
5209
5233
  for (let row = selection.area.start.row; !has_value && row <= selection.area.end.row; row++) {
5210
5234
  cell = cells.GetCell({ row, column: test.column }, false);
5211
- has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area));
5235
+ has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area || !!cell.spill));
5212
5236
  if (!has_value && cell && cell.merge_area) {
5213
5237
  cell = cells.GetCell(cell.merge_area.start, false);
5214
- has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area));
5238
+ has_value = has_value || (!!cell && (cell.type !== ValueType.undefined || !!cell.area || !!cell.spill));
5215
5239
  }
5216
5240
  }
5217
5241
  }
@@ -6557,15 +6581,24 @@ export class Grid extends GridBase {
6557
6581
  */
6558
6582
  private UpdateFormulaBarFormula(override?: string) {
6559
6583
 
6560
- if (!this.formula_bar) { return; }
6584
+ this.layout.HideDropdownCaret();
6585
+
6586
+ // NOTE: this means we won't set validation carets... that needs
6587
+ // to be handled separately (FIXME)
6588
+
6589
+ // if (!this.formula_bar) { return; }
6561
6590
 
6562
6591
  if (override) {
6563
- this.formula_bar.formula = override;
6592
+ if (this.formula_bar) {
6593
+ this.formula_bar.formula = override;
6594
+ }
6564
6595
  return;
6565
6596
  }
6566
6597
 
6567
6598
  if (this.primary_selection.empty) {
6568
- this.formula_bar.formula = '';
6599
+ if (this.formula_bar) {
6600
+ this.formula_bar.formula = '';
6601
+ }
6569
6602
  }
6570
6603
  else {
6571
6604
  let data = this.active_sheet.CellData(this.primary_selection.target);
@@ -6581,7 +6614,10 @@ export class Grid extends GridBase {
6581
6614
  }
6582
6615
  }
6583
6616
 
6584
- this.formula_bar.editable = !data.style?.locked;
6617
+ if (this.formula_bar) {
6618
+ this.formula_bar.editable = !data.style?.locked;
6619
+ }
6620
+
6585
6621
  const value = this.NormalizeCellValue(data);
6586
6622
 
6587
6623
  // this isn't necessarily the best place for this, except that
@@ -6611,17 +6647,19 @@ export class Grid extends GridBase {
6611
6647
  }
6612
6648
 
6613
6649
  }
6614
- else {
6615
- this.layout.HideDropdownCaret();
6616
- }
6617
6650
 
6618
- // add braces for area
6619
- if (data.area) {
6620
- this.formula_bar.formula = '{' + (value || '') + '}';
6621
- }
6622
- else {
6623
- this.formula_bar.formula = (typeof value !== 'undefined') ? value.toString() : ''; // value || ''; // what about zero?
6651
+ if (this.formula_bar) {
6652
+
6653
+ // add braces for area
6654
+ if (data.area) {
6655
+ this.formula_bar.formula = '{' + (value || '') + '}';
6656
+ }
6657
+ else {
6658
+ this.formula_bar.formula = (typeof value !== 'undefined') ? value.toString() : ''; // value || ''; // what about zero?
6659
+ }
6660
+
6624
6661
  }
6662
+
6625
6663
  }
6626
6664
 
6627
6665
  }
@@ -6869,14 +6907,20 @@ export class Grid extends GridBase {
6869
6907
 
6870
6908
  if (view && view.node) {
6871
6909
  // this.selected_annotation.node.innerHTML;
6872
- const node = view.node.firstChild;
6910
+ const node = view.node.firstChild?.firstChild;
6911
+
6873
6912
  if (node) {
6913
+
6914
+ // trying to put svg on the clipboard here, which works, but
6915
+ // is basically useless. the underlying method is good, though,
6916
+ // clients could use it for better UX in saving images
6917
+
6874
6918
  const html = (SerializeHTML(node as Element) as HTMLElement).outerHTML;
6875
6919
 
6876
- // no other format supported? (...)
6877
- const type = 'text/plain';
6878
- event.clipboardData.setData(type, html);
6879
- // console.info(html);
6920
+ event.clipboardData.setData('text/uri-list', `data:image/svg+xml;base64,` + btoa(html)); // <-- does this work? seems no
6921
+ event.clipboardData.setData('text/html', html); // <-- does this work? (also no)
6922
+ event.clipboardData.setData('text/plain', html);
6923
+
6880
6924
  }
6881
6925
  }
6882
6926
  }
@@ -208,6 +208,9 @@ export interface UnitAddress extends BaseUnit {
208
208
  absolute_row?: boolean;
209
209
  absolute_column?: boolean;
210
210
 
211
+ /** spill flag (address ends with #) */
212
+ spill?: boolean;
213
+
211
214
  /**
212
215
  * this means the row is a relative offset from the current row. this
213
216
  * happens if you use R1C1 syntax with square brackets.
@@ -970,7 +970,8 @@ export class Parser {
970
970
  (address.absolute_column ? '$' : '') +
971
971
  this.ColumnLabel(column) +
972
972
  (address.absolute_row ? '$' : '') +
973
- (row + 1)
973
+ (row + 1) +
974
+ (address.spill ? '#' : '')
974
975
  );
975
976
  }
976
977
 
@@ -2210,6 +2211,9 @@ export class Parser {
2210
2211
 
2211
2212
  || (char === QUESTION_MARK && square_bracket === 0)
2212
2213
 
2214
+ // moving
2215
+ // || (char === HASH) // FIXME: this should only be allowed at the end...
2216
+
2213
2217
  /*
2214
2218
 
2215
2219
  || (this.flags.r1c1 && (
@@ -2243,6 +2247,11 @@ export class Parser {
2243
2247
  else break;
2244
2248
  }
2245
2249
 
2250
+ // hash at end only
2251
+ if (this.data[this.index] === HASH) {
2252
+ token.push(this.data[this.index++]);
2253
+ }
2254
+
2246
2255
  const str = token.map((num) => String.fromCharCode(num)).join('');
2247
2256
 
2248
2257
  // special handling: unbalanced single quote (probably sheet name),
@@ -2523,6 +2532,7 @@ export class Parser {
2523
2532
  // as names. so this should be a token if r === 0.
2524
2533
 
2525
2534
  const r = this.ConsumeAddressRow(position);
2535
+
2526
2536
  if (!r) return null;
2527
2537
  position = r.position;
2528
2538
 
@@ -2544,6 +2554,7 @@ export class Parser {
2544
2554
  absolute_column: c.absolute,
2545
2555
  position: index,
2546
2556
  sheet,
2557
+ spill: r.spill,
2547
2558
  };
2548
2559
 
2549
2560
  // if that's not the complete token, then it's invalid
@@ -2578,6 +2589,8 @@ export class Parser {
2578
2589
  absolute: boolean;
2579
2590
  row: number;
2580
2591
  position: number;
2592
+ spill?: boolean; // spill reference
2593
+
2581
2594
  }|false {
2582
2595
 
2583
2596
  const absolute = this.data[position] === DOLLAR_SIGN;
@@ -2607,7 +2620,13 @@ export class Parser {
2607
2620
  return false;
2608
2621
  }
2609
2622
 
2610
- return { absolute, row: value - 1, position };
2623
+ let spill = false;
2624
+ if (this.data[position] === HASH) {
2625
+ position++;
2626
+ spill = true;
2627
+ }
2628
+
2629
+ return { absolute, row: value - 1, position, spill };
2611
2630
  }
2612
2631
 
2613
2632
  /**
@@ -19,25 +19,33 @@
19
19
  *
20
20
  */
21
21
 
22
+ /*
22
23
  interface StringMap {
23
24
  [index: string]: string;
24
25
  }
26
+ */
27
+
28
+ type StringMap = Map<string, string>;
29
+
25
30
 
26
31
  /**
27
32
  * defaults are global, since we assume they never change. created on demand.
28
33
  */
29
34
  let default_properties: StringMap|undefined;
30
35
 
36
+ /**
37
+ * convert CSSStyleDeclaration to map
38
+ */
31
39
  const PropertyMap = (source: CSSStyleDeclaration): StringMap => {
32
40
 
33
- const map: StringMap = {};
41
+ const map: StringMap = new Map();
34
42
 
35
43
  // you can iterate this thing, although apparently ts won't allow
36
44
  // it because it's not in the spec? should probably play ball
37
45
 
38
46
  for (let i = 0; i < source.length; i++) {
39
47
  const key = source[i];
40
- map[key] = source.getPropertyValue(key);
48
+ map.set(key, source.getPropertyValue(key));
41
49
  }
42
50
 
43
51
  return map;
@@ -49,16 +57,17 @@ const PropertyMap = (source: CSSStyleDeclaration): StringMap => {
49
57
  */
50
58
  const GetAppliedStyle = (node: Element, computed: CSSStyleDeclaration, defaults: StringMap) => {
51
59
 
52
- const applied: StringMap = {};
60
+ const applied: StringMap = new Map();
53
61
  const computed_map = PropertyMap(computed);
54
62
 
55
- for (const key of Object.keys(computed_map)) {
56
- if (computed_map[key] !== defaults[key]) {
57
- applied[key] = defaults[key];
63
+ for (const [key, value] of computed_map.entries()) {
64
+ if (value !== defaults.get(key)) {
65
+ applied.set(key, value);
58
66
  }
59
67
  }
60
68
 
61
- return (Object.keys(applied).map((key) => `${key}: ${applied[key]}`).join('; ') +
69
+ const arr = Array.from(applied.entries());
70
+ return (arr.map(([key, value]) => `${key}: ${value}`).join('; ') +
62
71
  '; ' + (node.getAttribute('style') || '')).trim().replace(/"/g, '\'');
63
72
 
64
73
  };
@@ -118,7 +127,7 @@ export const SerializeHTML = (node: Element) => {
118
127
 
119
128
  if (!default_properties) {
120
129
 
121
- const defaults: StringMap = {};
130
+ const defaults: StringMap = new Map();
122
131
 
123
132
  // regarding document, in this case we're creating an iframe
124
133
  // specifically for isolation, and adding it to "document".
@@ -137,7 +146,7 @@ export const SerializeHTML = (node: Element) => {
137
146
  const div = frame_document.createElement('div');
138
147
  frame_document.body.appendChild(div);
139
148
  const computed = getComputedStyle(div);
140
- Array.prototype.forEach.call(computed, (key) => defaults[key] = computed[key]);
149
+ Array.prototype.forEach.call(computed, key => defaults.set(key, computed[key]));
141
150
  }
142
151
 
143
152
  document.body.removeChild(iframe);
@@ -145,7 +154,23 @@ export const SerializeHTML = (node: Element) => {
145
154
 
146
155
  }
147
156
 
148
- return RenderNode(node, default_properties);
157
+ const rendered = RenderNode(node, default_properties);
158
+ if (rendered instanceof Element && rendered.tagName === 'svg') {
159
+ if (!rendered.hasAttribute('version')) {
160
+ rendered.setAttribute('version', '1.1');
161
+ }
162
+
163
+ if (!rendered.hasAttribute('xmlns')) {
164
+ rendered.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
165
+ }
166
+
167
+ if (!rendered.hasAttribute('xmlns:xlink')) {
168
+ rendered.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
169
+ }
170
+
171
+ }
172
+
173
+ return rendered;
149
174
 
150
175
  };
151
176