@trebco/treb 29.8.4 → 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 +61 -25
  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');
@@ -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
  }
@@ -5099,13 +5107,16 @@ export class Grid extends GridBase {
5099
5107
 
5100
5108
  const cell = this.active_sheet.CellData(this.primary_selection.target);
5101
5109
 
5102
- if (!cell || (!cell.area && !cell.table)) {
5110
+ if (!cell || (!cell.area && !cell.table && !cell.spill)) {
5103
5111
  return;
5104
5112
  }
5105
5113
 
5106
5114
  if (cell.area) {
5107
5115
  this.Select(this.primary_selection, cell.area, cell.area.start);
5108
5116
  }
5117
+ if (cell.spill) {
5118
+ this.Select(this.primary_selection, cell.spill, cell.spill.start);
5119
+ }
5109
5120
  if (cell.table) {
5110
5121
  const area = new Area(cell.table.area.start, cell.table.area.end);
5111
5122
  this.Select(this.primary_selection, area, area.start);
@@ -5123,7 +5134,12 @@ export class Grid extends GridBase {
5123
5134
 
5124
5135
  const show_primary_selection = this.hide_selection ? false :
5125
5136
  (!this.editing_state) || (this.editing_cell.sheet_id === this.active_sheet.id);
5126
-
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
+
5127
5143
  this.selection_renderer?.RenderSelections(show_primary_selection, rerender);
5128
5144
  }
5129
5145
 
@@ -5172,7 +5188,7 @@ export class Grid extends GridBase {
5172
5188
  const cells = this.active_sheet.cells;
5173
5189
 
5174
5190
  let cell = cells.GetCell(selection.target, false);
5175
- if (!cell || (cell.type === ValueType.undefined && !cell.area)) {
5191
+ if (!cell || (cell.type === ValueType.undefined && !cell.area && !cell.spill)) {
5176
5192
  return false;
5177
5193
  }
5178
5194
 
@@ -5206,20 +5222,20 @@ export class Grid extends GridBase {
5206
5222
  if (rows) {
5207
5223
  for (let column = selection.area.start.column; !has_value && column <= selection.area.end.column; column++) {
5208
5224
  cell = cells.GetCell({ row: test.row, column }, false);
5209
- 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));
5210
5226
  if (!has_value && cell && cell.merge_area) {
5211
5227
  cell = cells.GetCell(cell.merge_area.start, false);
5212
- 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));
5213
5229
  }
5214
5230
  }
5215
5231
  }
5216
5232
  else {
5217
5233
  for (let row = selection.area.start.row; !has_value && row <= selection.area.end.row; row++) {
5218
5234
  cell = cells.GetCell({ row, column: test.column }, false);
5219
- 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));
5220
5236
  if (!has_value && cell && cell.merge_area) {
5221
5237
  cell = cells.GetCell(cell.merge_area.start, false);
5222
- 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));
5223
5239
  }
5224
5240
  }
5225
5241
  }
@@ -6565,15 +6581,24 @@ export class Grid extends GridBase {
6565
6581
  */
6566
6582
  private UpdateFormulaBarFormula(override?: string) {
6567
6583
 
6568
- 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; }
6569
6590
 
6570
6591
  if (override) {
6571
- this.formula_bar.formula = override;
6592
+ if (this.formula_bar) {
6593
+ this.formula_bar.formula = override;
6594
+ }
6572
6595
  return;
6573
6596
  }
6574
6597
 
6575
6598
  if (this.primary_selection.empty) {
6576
- this.formula_bar.formula = '';
6599
+ if (this.formula_bar) {
6600
+ this.formula_bar.formula = '';
6601
+ }
6577
6602
  }
6578
6603
  else {
6579
6604
  let data = this.active_sheet.CellData(this.primary_selection.target);
@@ -6589,7 +6614,10 @@ export class Grid extends GridBase {
6589
6614
  }
6590
6615
  }
6591
6616
 
6592
- this.formula_bar.editable = !data.style?.locked;
6617
+ if (this.formula_bar) {
6618
+ this.formula_bar.editable = !data.style?.locked;
6619
+ }
6620
+
6593
6621
  const value = this.NormalizeCellValue(data);
6594
6622
 
6595
6623
  // this isn't necessarily the best place for this, except that
@@ -6619,17 +6647,19 @@ export class Grid extends GridBase {
6619
6647
  }
6620
6648
 
6621
6649
  }
6622
- else {
6623
- this.layout.HideDropdownCaret();
6624
- }
6625
6650
 
6626
- // add braces for area
6627
- if (data.area) {
6628
- this.formula_bar.formula = '{' + (value || '') + '}';
6629
- }
6630
- else {
6631
- 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
+
6632
6661
  }
6662
+
6633
6663
  }
6634
6664
 
6635
6665
  }
@@ -6877,14 +6907,20 @@ export class Grid extends GridBase {
6877
6907
 
6878
6908
  if (view && view.node) {
6879
6909
  // this.selected_annotation.node.innerHTML;
6880
- const node = view.node.firstChild;
6910
+ const node = view.node.firstChild?.firstChild;
6911
+
6881
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
+
6882
6918
  const html = (SerializeHTML(node as Element) as HTMLElement).outerHTML;
6883
6919
 
6884
- // no other format supported? (...)
6885
- const type = 'text/plain';
6886
- event.clipboardData.setData(type, html);
6887
- // 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
+
6888
6924
  }
6889
6925
  }
6890
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