@trebco/treb 23.6.2 → 25.0.0-rc1

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 (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +293 -299
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,2204 @@
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-2023 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ /**
23
+ * rewrite of export. we'll still use a template, but do more direct
24
+ * writing and less DOM manipulation. this should be cleaner in the long
25
+ * run, but it will take a bit more work.
26
+ */
27
+
28
+ import JSZip from 'jszip';
29
+
30
+ import { PixelsToColumnWidth } from './column-width';
31
+
32
+ const XMLDeclaration = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n`;
33
+
34
+ import { template } from './template-2';
35
+ import type { SerializedSheet } from 'treb-grid';
36
+
37
+ import { IArea, Area, ICellAddress, Cells, ValueType, CellValue, Style, DataValidation, ValidationType,
38
+ AnnotationLayout, Corner as LayoutCorner, ICellAddress2, Table, Cell } from 'treb-base-types';
39
+
40
+ // import * as xmlparser from 'fast-xml-parser';
41
+ import { XMLParser, XmlBuilderOptions, XMLBuilder } from 'fast-xml-parser';
42
+
43
+ import { SharedStrings } from './shared-strings2';
44
+ import { StyleCache, XlColor, BorderEdge } from './workbook-style2';
45
+ import { Theme } from './workbook-theme2';
46
+
47
+ import { RelationshipMap, AddRel } from './relationship';
48
+ import { XMLOptions2 } from './xml-utils';
49
+
50
+ import { Parser, UnitAddress, UnitRange, ExpressionUnit, IllegalSheetNameRegex, QuotedSheetNameRegex } from 'treb-parser';
51
+
52
+ // FIXME: move
53
+ import { Chart, ChartOptions } from './drawing2/chart2';
54
+ import type { ImageOptions } from './drawing2/embedded-image';
55
+ import { Drawing, TwoCellAnchor } from './drawing2/drawing2';
56
+ import type { TableDescription, TableFooterType } from './workbook2';
57
+
58
+ export class Exporter {
59
+
60
+ public zip?: JSZip;
61
+
62
+ public xmloptions: Partial<XmlBuilderOptions> = {
63
+ format: true,
64
+ attributesGroupName: 'a$',
65
+ textNodeName: 't$',
66
+ ignoreAttributes: false,
67
+ suppressEmptyNode: true,
68
+
69
+ // OK so now I am turning this off altogether. not sure why we
70
+ // were using it in the first place -- which is a problem, since
71
+ // there's probably something I don't know.
72
+
73
+ /*
74
+ tagValueProcessor: (name: string, a: string) => {
75
+
76
+ // we were including unsafe symbols here, but that was
77
+ // resulting in double-encoding. not sure why this is
78
+ // here at all, unless we need it for unicode? in any
79
+ // event (atm) allowing unsafe symbols is sufficient
80
+
81
+ return a; // ?
82
+
83
+ return (typeof a === 'string') ? he.encode(a, { useNamedReferences: true, allowUnsafeSymbols: true }) : a;
84
+ },
85
+ */
86
+
87
+ // there's a "isAttributeValue" for decode, but no option for encode?
88
+ // we only want to encode ' and "
89
+
90
+ // attrValueProcessor: a => (typeof a === 'string') ? he.encode(a, { useNamedReferences: true }) : a,
91
+
92
+ // why is this double-encoding? is there arlready implicit encoding? (...)
93
+ // there must have been a reason we used it in the first place... but I don't know what that was.
94
+ // do we need to encode apostrophes?
95
+
96
+ // attributeValueProcessor: (name: string, a: string) => (typeof a === 'string') ?
97
+ // a.replace(/"/g, '&quot;').replace(/'/g, '&apos;') : a,
98
+
99
+
100
+
101
+ };
102
+
103
+ // public xmlparser = new xmlparser.j2xParser(this.xmloptions);
104
+ public xmlbuilder1 = new XMLBuilder(this.xmloptions);
105
+ public xmlparser2 = new XMLParser(XMLOptions2);
106
+
107
+ // FIXME: need a way to share/pass parser flags
108
+ public parser = new Parser();
109
+
110
+ public decorated_functions: Record<string, string> = {};
111
+
112
+ /*
113
+ constructor() {
114
+
115
+ }
116
+ */
117
+
118
+ /**
119
+ * init used to load the template file. we added a parameter to
120
+ * pass in the list of functions that need decoration (_xlfn).
121
+ *
122
+ * @param decorated_functions
123
+ */
124
+ public async Init(decorated_functions: Record<string, string> = {}): Promise<void> {
125
+
126
+ // this.decorated_functions = decorated_functions.map(name => name.toLowerCase()); // normalized
127
+
128
+ for (const key of Object.keys(decorated_functions)) {
129
+ this.decorated_functions[key.toLowerCase()] = decorated_functions[key]; // normalized
130
+ }
131
+
132
+ this.zip = await new JSZip().loadAsync(template, {base64: true});
133
+
134
+ }
135
+
136
+ public async WriteRels(rels: RelationshipMap, path: string, dump = false): Promise<void> {
137
+
138
+ if (!this.zip) {
139
+ throw new Error('missing zip');
140
+ }
141
+
142
+ const keys = Object.keys(rels);
143
+
144
+ const dom = {
145
+ Relationships: {
146
+ a$: {
147
+ xmlns: 'http://schemas.openxmlformats.org/package/2006/relationships',
148
+ },
149
+ Relationship: keys.map(key => {
150
+ const rel = rels[key];
151
+ const a$: any = {
152
+ Id: rel.id,
153
+ Target: rel.target,
154
+ Type: rel.type,
155
+ };
156
+ if (rel.mode) {
157
+ a$.TargetMode = rel.mode;
158
+ }
159
+ return { a$ };
160
+ }),
161
+ },
162
+ };
163
+
164
+ const xml = XMLDeclaration + this.xmlbuilder1.build(dom);
165
+
166
+ // console.info({dom, xml});
167
+
168
+ if (dump) {
169
+ console.info(xml);
170
+ }
171
+ await this.zip?.file(path, xml);
172
+
173
+ }
174
+
175
+ /**
176
+ * format and write styles
177
+ */
178
+ public async WriteStyleCache(style_cache: StyleCache): Promise<void> {
179
+
180
+ if (!this.zip) {
181
+ throw new Error('missing zip');
182
+ }
183
+
184
+ const ColorAttributes = (color: XlColor) => {
185
+
186
+ // we could just pass through except that we have argb and excel has rgb
187
+
188
+ const attrs: any = {};
189
+ if (color.indexed !== undefined) {
190
+ attrs.indexed = color.indexed;
191
+ }
192
+ if (color.theme !== undefined) {
193
+ attrs.theme = color.theme;
194
+ }
195
+ if (color.tint !== undefined) {
196
+ attrs.tint = color.tint;
197
+ }
198
+ if (color.argb !== undefined) {
199
+ attrs.rgb = color.argb;
200
+ }
201
+ return attrs;
202
+ };
203
+
204
+ const xfs = style_cache.cell_xfs.map(xf => {
205
+ const block: any = {
206
+ a$: {
207
+ numFmtId: xf.number_format,
208
+ fontId: xf.font,
209
+ fillId: xf.fill,
210
+ borderId: xf.border,
211
+ },
212
+ };
213
+
214
+ if (xf.horizontal_alignment || xf.vertical_alignment || xf.wrap_text) {
215
+ // block.a$.applyAlignment = 1;
216
+ block.alignment = { a$: {}};
217
+ if (xf.horizontal_alignment) {
218
+ block.alignment.a$.horizontal = xf.horizontal_alignment;
219
+ }
220
+ if (xf.vertical_alignment) {
221
+ block.alignment.a$.vertical = xf.vertical_alignment;
222
+ }
223
+ if (xf.wrap_text) {
224
+ block.alignment.a$.wrapText = 1;
225
+ }
226
+ }
227
+
228
+ return block;
229
+ });
230
+
231
+ const BorderColorAttributes = (edge: BorderEdge) => {
232
+ if (edge.color) {
233
+ return { indexed: edge.color };
234
+ }
235
+ if (edge.rgba) {
236
+ return { rgb: edge.rgba };
237
+ }
238
+ if (edge.theme) {
239
+ const attrs: any = {
240
+ theme: edge.theme,
241
+ }
242
+ if (edge.tint) {
243
+ attrs.tint = edge.tint;
244
+ }
245
+ return attrs;
246
+ }
247
+ return undefined;
248
+ };
249
+
250
+ const borders = style_cache.borders.map(border => {
251
+ const block: any = {
252
+ left: {},
253
+ right: {},
254
+ top: {},
255
+ bottom: {},
256
+ diagonal: {},
257
+ };
258
+
259
+ if (border.top.style) {
260
+ block.top.a$ = {
261
+ style: border.top.style,
262
+ };
263
+ const attrs = BorderColorAttributes(border.top);
264
+ if (attrs) { block.top.color = {a$: attrs}; }
265
+ }
266
+
267
+ if (border.left.style) {
268
+ block.left.a$ = {
269
+ style: border.left.style,
270
+ };
271
+ const attrs = BorderColorAttributes(border.left);
272
+ if (attrs) { block.left.color = {a$: attrs}; }
273
+ }
274
+
275
+ if (border.bottom.style) {
276
+ block.bottom.a$ = {
277
+ style: border.bottom.style,
278
+ };
279
+ const attrs = BorderColorAttributes(border.bottom);
280
+ if (attrs) { block.bottom.color = {a$: attrs}; }
281
+ }
282
+
283
+ if (border.right.style) {
284
+ block.right.a$ = {
285
+ style: border.right.style,
286
+ };
287
+ const attrs = BorderColorAttributes(border.right);
288
+ if (attrs) { block.right.color = {a$: attrs}; }
289
+ }
290
+
291
+ if (border.diagonal.style) {
292
+ block.diagonal.a$ = {
293
+ style: border.diagonal.style,
294
+ };
295
+ const attrs = BorderColorAttributes(border.diagonal);
296
+ if (attrs) { block.diagonal.color = {a$: attrs}; }
297
+ }
298
+
299
+ return block;
300
+ });
301
+
302
+ // console.info("SC", style_cache);
303
+
304
+ const fills = style_cache.fills.map(fill => {
305
+ const block: any = {
306
+ a$: { patternType: fill.pattern_type },
307
+ };
308
+
309
+ if (fill.pattern_gray !== undefined) {
310
+ block.a$.patternType = `gray${fill.pattern_gray}`;
311
+ }
312
+ if (fill.bg_color) {
313
+ block.bgColor = { a$: ColorAttributes(fill.bg_color) };
314
+ }
315
+ if (fill.fg_color) {
316
+ block.fgColor = { a$: ColorAttributes(fill.fg_color) };
317
+ }
318
+
319
+ return {patternFill: block};
320
+ });
321
+
322
+ const fonts = style_cache.fonts.map(font => {
323
+ const block: any = {};
324
+
325
+ // flags
326
+
327
+ if (font.bold) { block.b = ''; }
328
+ if (font.italic) { block.i = ''; }
329
+ if (font.underline) { block.u = ''; }
330
+ if (font.strike) { block.strike = ''; }
331
+
332
+ // "val" props
333
+
334
+ if (font.size !== undefined) {
335
+ block.sz = { a$: { val: font.size }};
336
+ }
337
+ if (font.family !== undefined) {
338
+ block.family = { a$: { val: font.family }};
339
+ }
340
+ if (font.name !== undefined) {
341
+ block.name = { a$: { val: font.name }};
342
+ }
343
+ if (font.scheme !== undefined) {
344
+ block.scheme = { a$: { val: font.scheme }};
345
+ }
346
+
347
+ // color
348
+
349
+ if (font.color_argb !== undefined) {
350
+ block.color = { a$: { rgb: font.color_argb }};
351
+ }
352
+ else if (font.color_theme !== undefined) {
353
+ block.color = { a$: { theme: font.color_theme }};
354
+ if (font.color_tint) {
355
+ block.color.a$.tint = font.color_tint;
356
+ }
357
+ }
358
+
359
+ return block;
360
+ });
361
+
362
+ const dom: any = {
363
+ styleSheet: {
364
+ a$: {
365
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
366
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
367
+ 'mc:Ignorable': 'x14ac x16r2 xr',
368
+ 'xmlns:x14ac': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac',
369
+ 'xmlns:x16r2': 'http://schemas.microsoft.com/office/spreadsheetml/2015/02/main',
370
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
371
+ },
372
+ },
373
+ };
374
+
375
+ // we're only adding elements here if they are not empty, but in
376
+ // practice only numFmts can be empty (because there are implicit
377
+ // formats); everything else has a default 0 entry
378
+
379
+ if (style_cache.number_formats.length) {
380
+
381
+ dom.styleSheet.numFmts = {
382
+ a$: { count: style_cache.number_formats.length },
383
+ numFmt: style_cache.number_formats.map(format => {
384
+ return {
385
+ a$: {
386
+ numFmtId: format.id,
387
+ formatCode: format.format,
388
+ },
389
+ };
390
+ }),
391
+ };
392
+
393
+ // console.info(style_cache.number_formats);
394
+ // console.info(dom.styleSheet.numFmts);
395
+
396
+ }
397
+
398
+ if (fonts.length) {
399
+ dom.styleSheet.fonts = {
400
+ a$: { count: fonts.length },
401
+ font: fonts,
402
+ };
403
+ }
404
+
405
+ if (fills.length) {
406
+ dom.styleSheet.fills = {
407
+ a$: { count: fills.length },
408
+ fill: fills,
409
+ };
410
+ }
411
+
412
+ if (borders.length) {
413
+ dom.styleSheet.borders = {
414
+ a$: { count: borders.length },
415
+ border: borders,
416
+ };
417
+ }
418
+
419
+ // console.info("B", borders, JSON.stringify(dom.styleSheet.borders, undefined, 2))
420
+
421
+ if (xfs.length) {
422
+ dom.styleSheet.cellXfs = {
423
+ a$: { count: xfs.length },
424
+ xf: xfs,
425
+ };
426
+ }
427
+
428
+ // not used:
429
+ //
430
+ // cellStyleXfs
431
+ // cellStyles
432
+ // dxfs
433
+ // tableStyles
434
+
435
+ // ------------
436
+
437
+ const xml = XMLDeclaration + this.xmlbuilder1.build(dom);
438
+ // console.info(xml);
439
+
440
+ await this.zip?.file('xl/styles.xml', xml);
441
+
442
+ }
443
+
444
+ /**
445
+ * format and write shared strings file to the zip archive. this will
446
+ * replace any existing shared strings file.
447
+ */
448
+ public async WriteSharedStrings(shared_strings: SharedStrings): Promise<void> {
449
+
450
+ // console.info({shared_strings});
451
+
452
+ if (!this.zip) {
453
+ throw new Error('missing zip');
454
+ }
455
+
456
+ const dom: any = {
457
+ sst: {
458
+ a$: {
459
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
460
+ count: shared_strings.strings.length,
461
+ uniqueCount: shared_strings.strings.length,
462
+ },
463
+ si: [
464
+ ...shared_strings.strings.map(t => { return {t}}),
465
+ ],
466
+ },
467
+ };
468
+
469
+ const xml = XMLDeclaration + this.xmlbuilder1.build(dom);
470
+
471
+ // console.info(xml);
472
+
473
+ await this.zip?.file('xl/sharedStrings.xml', xml);
474
+
475
+ }
476
+
477
+ /**
478
+ * FIXME: merge with workbook function (put somewhere else)
479
+ * /
480
+ public async ReadRels(zip?: JSZip, path = ''): Promise<RelationshipMap> {
481
+
482
+ const rels: RelationshipMap = {};
483
+ const data = await zip?.file(path)?.async('text') as string;
484
+ //
485
+ // force array on <Relationship/> elements, but be slack on the rest
486
+ // (we know they are single elements)
487
+ //
488
+ const xml = this.xmlparser2.parse(data || '');
489
+ console.info(path, xml);
490
+
491
+ for (const relationship of xml.Relationships?.Relationship || []) {
492
+ const id = relationship.a$.Id;
493
+ rels[id] = {
494
+ id,
495
+ type: relationship.a$.Type,
496
+ target: relationship.a$.Target,
497
+ };
498
+ }
499
+
500
+ return rels;
501
+
502
+ }
503
+ */
504
+
505
+ public StyleFromCell(sheet: SerializedSheet, style_cache: StyleCache, row: number, column: number, style: Style.Properties = {}) {
506
+
507
+ //if (row === 2 && column === 5)
508
+ // console.info("SFC", JSON.stringify(style, undefined, 2));
509
+
510
+ const cell_style_refs = sheet.styles || sheet.cell_style_refs || [];
511
+
512
+ const list: Style.Properties[] = [sheet.sheet_style];
513
+
514
+ if (sheet.row_pattern && sheet.row_pattern.length) {
515
+ list.push(sheet.row_pattern[row % sheet.row_pattern.length]);
516
+ }
517
+
518
+ // is this backwards, vis a vis our rendering? I think it might be...
519
+ // YES: should be row pattern -> row -> column -> cell [corrected]
520
+
521
+ // if (sheet.row_style && sheet.row_style[row]) {
522
+ // list.push(sheet.row_style[row]);
523
+ // }
524
+
525
+ if (sheet.row_style) {
526
+ let style = sheet.row_style[row];
527
+ if (typeof style === 'number') {
528
+ style = cell_style_refs[style];
529
+ if (style) {
530
+ list.push(style);
531
+ }
532
+ }
533
+ else if (style) {
534
+ list.push(style);
535
+ }
536
+ }
537
+
538
+ // this can now be a number, and possibly 0 (?)
539
+
540
+ // actually 0 is by default a null style, although that's more of
541
+ // a convention than a hard rule, not sure we should rely on it
542
+
543
+ if (sheet.column_style) {
544
+ let style = sheet.column_style[column];
545
+ if (typeof style === 'number') {
546
+ style = cell_style_refs[style];
547
+ if (style) {
548
+ list.push(style);
549
+ }
550
+ }
551
+ else if (style) {
552
+ list.push(style);
553
+ }
554
+ }
555
+
556
+ //if (sheet.column_style && sheet.column_style[column]) {
557
+ // list.push(sheet.column_style[column]);
558
+ //}
559
+
560
+ /*
561
+ if (cell.ref) {
562
+ list.push(sheet_source.cell_style_refs[cell.ref]);
563
+ }
564
+ else if (cell.style_ref) {
565
+ list.push(sheet_source.cell_style_refs[cell.style_ref]);
566
+ }
567
+ else if (style_map[cell.column] && style_map[cell.column][cell.row]) {
568
+ list.push(style_map[cell.column][cell.row]);
569
+ }
570
+ */
571
+ list.push(style);
572
+
573
+ const options = style_cache.StyleOptionsFromProperties(Style.Composite(list));
574
+
575
+ return style_cache.EnsureStyle(options);
576
+
577
+ }
578
+
579
+ /** overload for return type */
580
+ public NormalizeAddress(unit: UnitAddress, sheet: SerializedSheet): UnitAddress;
581
+
582
+ /** overload for return type */
583
+ public NormalizeAddress(unit: UnitRange, sheet: SerializedSheet): UnitRange;
584
+
585
+ /**
586
+ * for charts we need addresses to be absolute ($) and ensure there's
587
+ * a sheet name -- use the active sheet if it's not explicitly referenced
588
+ */
589
+ public NormalizeAddress(unit: UnitAddress|UnitRange, sheet: SerializedSheet): UnitAddress|UnitRange {
590
+
591
+ const addresses = (unit.type === 'address') ? [unit] : [unit.start, unit.end];
592
+
593
+ for (const address of addresses) {
594
+ address.absolute_row = true;
595
+ address.absolute_column = true;
596
+ if (!address.sheet) {
597
+ address.sheet = sheet.name;
598
+ }
599
+ }
600
+ if (unit.type === 'range') {
601
+ unit.end.sheet = undefined;
602
+ }
603
+
604
+ unit.label = this.parser.Render(unit);
605
+ return unit; // fluent
606
+
607
+ }
608
+
609
+
610
+ /**
611
+ * new-style annotation layout (kind of a two-cell anchor) to two-cell anchor
612
+ */
613
+ public AnnotationLayoutToAnchor(layout: AnnotationLayout, sheet: SerializedSheet): TwoCellAnchor {
614
+
615
+ // our offsets are % of cell. their offsets are in excel units,
616
+ // but when the chart is added our method will convert from pixels.
617
+
618
+ const address_to_anchor = (corner: LayoutCorner) => {
619
+
620
+ const width = (sheet.column_width && sheet.column_width[corner.address.column]) ?
621
+ sheet.column_width[corner.address.column] : (sheet.default_column_width || 100);
622
+
623
+ const height = (sheet.row_height && sheet.row_height[corner.address.row]) ?
624
+ sheet.row_height[corner.address.row] : (sheet.default_row_height || 20);
625
+
626
+ return {
627
+ ...corner.address,
628
+ row_offset: Math.round(corner.offset.y * height),
629
+ column_offset: Math.round(corner.offset.x * width),
630
+ };
631
+
632
+ };
633
+
634
+ return {
635
+ from: address_to_anchor(layout.tl),
636
+ to: address_to_anchor(layout.br),
637
+ };
638
+
639
+ }
640
+
641
+ /**
642
+ * convert a rectangle (pixels) to a two-cell anchor. note that
643
+ * our offsets are in pixels, they'll need to be changed to whatever
644
+ * the target units are.
645
+ */
646
+ public AnnotationRectToAnchor(
647
+ annotation_rect: {
648
+ left: number;
649
+ top: number;
650
+ width: number;
651
+ height: number;
652
+ },
653
+ sheet: SerializedSheet): TwoCellAnchor {
654
+
655
+ const anchor: TwoCellAnchor = {
656
+ from: {row: -1, column: -1},
657
+ to: {row: -1, column: -1},
658
+ };
659
+
660
+ const rect = {
661
+ ...annotation_rect, // {top, left, width, height}
662
+ right: annotation_rect.left + annotation_rect.width,
663
+ bottom: annotation_rect.top + annotation_rect.height,
664
+ };
665
+
666
+ for (let x = 0, column = 0; column < 1000; column++) {
667
+ const width = (sheet.column_width && sheet.column_width[column]) ? sheet.column_width[column] : (sheet.default_column_width || 100);
668
+ if (anchor.from.column < 0 && rect.left <= x + width) {
669
+ anchor.from.column = column;
670
+ anchor.from.column_offset = (rect.left - x);
671
+ }
672
+ if (anchor.to.column < 0 && rect.right <= x + width) {
673
+ anchor.to.column = column;
674
+ anchor.to.column_offset = (rect.right - x);
675
+ break;
676
+ }
677
+ x += width;
678
+ }
679
+
680
+ for (let y = 0, row = 0; row < 1000; row++) {
681
+ const height = (sheet.row_height && sheet.row_height[row]) ? sheet.row_height[row] : (sheet.default_row_height || 20);
682
+ if (anchor.from.row < 0 && rect.top <= y + height) {
683
+ anchor.from.row = row;
684
+ anchor.from.row_offset = (rect.top - y);
685
+ }
686
+ if (anchor.to.row < 0 && rect.bottom <= y + height) {
687
+ anchor.to.row = row;
688
+ anchor.to.row_offset = (rect.bottom - y);
689
+ break;
690
+ }
691
+ y += height;
692
+ }
693
+
694
+ return anchor;
695
+
696
+ }
697
+
698
+ public ParseImages(sheet_source: SerializedSheet): Array<{ anchor: TwoCellAnchor, options: ImageOptions }> {
699
+
700
+ const images: Array<{ anchor: TwoCellAnchor, options: ImageOptions }> = [];
701
+
702
+ for (const annotation of sheet_source.annotations || []) {
703
+ if (annotation.type === 'image' && annotation.data?.src) {
704
+
705
+ // this is (should be) a data URI in base64. at least (atm)
706
+ // that's all we support for exporting.
707
+
708
+ const src = annotation.data.src;
709
+ const match = src.match(/^data:image\/([^;]*?);base64,/);
710
+
711
+ if (match) {
712
+
713
+ const data = src.substr(match[0].length);
714
+ const mimetype = match[1];
715
+
716
+ const options: ImageOptions = {
717
+ data,
718
+ mimetype,
719
+ encoding: 'base64',
720
+ }
721
+
722
+ switch (mimetype) {
723
+ case 'svg+xml':
724
+ case 'webp':
725
+ case 'jpeg':
726
+ case 'jpg':
727
+ case 'image/png':
728
+ case 'png':
729
+ case 'gif':
730
+
731
+ if (annotation.rect) {
732
+ images.push({
733
+ anchor: this.AnnotationRectToAnchor(annotation.rect, sheet_source), options});
734
+ }
735
+ else if (annotation.layout) {
736
+ images.push({
737
+ anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
738
+ }
739
+ else {
740
+ console.warn('annotation missing layout');
741
+ }
742
+
743
+ break;
744
+
745
+ default:
746
+ console.info('unhandled image type', mimetype);
747
+ break;
748
+ }
749
+
750
+ }
751
+
752
+ }
753
+ }
754
+
755
+ return images;
756
+
757
+ }
758
+
759
+ public ParseCharts(sheet_source: SerializedSheet): Array<{ anchor: TwoCellAnchor, options: ChartOptions }> {
760
+
761
+ const charts: Array<{
762
+ anchor: TwoCellAnchor,
763
+ options: ChartOptions,
764
+ }> = [];
765
+
766
+ const parse_series = (arg: ExpressionUnit, options: ChartOptions, ref?: string) => {
767
+
768
+ if (arg.type === 'range') {
769
+ options.data.push(this.NormalizeAddress(arg, sheet_source));
770
+ }
771
+ else if (arg.type === 'call') {
772
+ if (/group/i.test(arg.name)) {
773
+ // recurse
774
+ for (const value of (arg.args || [])) {
775
+ parse_series(value, options, ref ? ref + ` (recurse)` : undefined);
776
+ }
777
+ }
778
+ else if (/series/i.test(arg.name)) {
779
+
780
+ const [label, x, y] = arg.args; // y is required
781
+
782
+ if (y && y.type === 'range') {
783
+ options.data.push(this.NormalizeAddress(y, sheet_source));
784
+
785
+ if (label) {
786
+
787
+ if (!options.names) { options.names = []; }
788
+
789
+ if (label.type === 'address') {
790
+ this.NormalizeAddress(label, sheet_source);
791
+ }
792
+
793
+ if (label.type === 'range') {
794
+ this.NormalizeAddress(label.start, sheet_source);
795
+ options.names[options.data.length - 1] = label.start;
796
+ }
797
+ else {
798
+ options.names[options.data.length - 1] = label;
799
+ }
800
+
801
+ }
802
+
803
+ if (!options.labels2) { options.labels2 = []; }
804
+ if (x && x.type === 'range') {
805
+ options.labels2[options.data.length - 1] = this.NormalizeAddress(x, sheet_source);
806
+ }
807
+ }
808
+ else {
809
+ console.info('invalid series missing Y', {y, arg, ref});
810
+ }
811
+
812
+ }
813
+ }
814
+
815
+ };
816
+
817
+ for (const annotation of sheet_source.annotations || []) {
818
+
819
+ const parse_result = this.parser.Parse(annotation.formula || '');
820
+ if (parse_result.expression && parse_result.expression.type === 'call') {
821
+
822
+ let type = '';
823
+ switch (parse_result.expression.name.toLowerCase()) {
824
+ case 'line.chart':
825
+ type = 'scatter';
826
+ break;
827
+ case 'scatter.line':
828
+ type = 'scatter2';
829
+ break;
830
+ case 'donut.chart':
831
+ type = 'donut';
832
+ break;
833
+ case 'bar.chart':
834
+ type = 'bar';
835
+ break;
836
+ case 'column.chart':
837
+ type = 'column';
838
+ break;
839
+ }
840
+
841
+ if (type === 'column' || type === 'donut' || type === 'bar' || type === 'scatter' || type === 'scatter2') {
842
+
843
+ const options: ChartOptions = { type, data: [] };
844
+
845
+ const title_index = (type === 'scatter2') ? 1 : 2;
846
+ const title_arg = parse_result.expression.args[title_index];
847
+
848
+ if (title_arg && title_arg.type === 'literal') {
849
+ options.title = title_arg;
850
+ }
851
+ else if (title_arg && title_arg.type === 'address') {
852
+ options.title = this.NormalizeAddress(title_arg, sheet_source);
853
+ }
854
+ else {
855
+
856
+ // FIXME: formula here will not work. we need to bring
857
+ // a calculator into this class? (!) or somehow cache the value...
858
+
859
+ // console.info('chart title arg', title_arg)
860
+ }
861
+
862
+ // we changed our Series() to Group(), and then added a new Series()
863
+ // function which adds data labels and per-series X values... will
864
+ // need to incorporate somehow. for now, just s/series/group to get
865
+ // the data in the chart
866
+
867
+ // oh we already did that... duh
868
+
869
+ if (parse_result.expression.args[0]) {
870
+ const arg0 = parse_result.expression.args[0];
871
+ if (type === 'scatter2' || type === 'bar' || type === 'column' || type === 'scatter') {
872
+ parse_series(arg0, options, sheet_source.name);
873
+ }
874
+ else if (arg0.type === 'range') {
875
+ options.data.push(this.NormalizeAddress(arg0, sheet_source));
876
+ }
877
+
878
+ // so the next cases cannot happen? (...) donut? (...)
879
+
880
+ else if (arg0.type === 'call' && /group/i.test(arg0.name)) {
881
+ for (const series of arg0.args) {
882
+
883
+ // in group, could be a range or a Series()
884
+ if (series.type === 'range') {
885
+ options.data.push(this.NormalizeAddress(series, sheet_source));
886
+ }
887
+ else if (series.type === 'call' && /series/i.test(series.name)) {
888
+
889
+ // in Series(), args are (name, X, range of data)
890
+
891
+ if (series.args[2] && series.args[2].type === 'range') {
892
+ options.data.push(this.NormalizeAddress(series.args[2], sheet_source));
893
+ }
894
+ }
895
+ }
896
+ }
897
+ else if (arg0.type === 'call' && /series/i.test(arg0.name)) {
898
+
899
+ // another case, single Series()
900
+ if (arg0.args[2] && arg0.args[2].type === 'range') {
901
+ options.data.push(this.NormalizeAddress(arg0.args[2], sheet_source));
902
+ }
903
+ }
904
+
905
+ /*
906
+ else if (arg0.type === 'call' && /series/i.test(arg0.name)) {
907
+ for (const series of arg0.args) {
908
+ if (series.type === 'range') {
909
+ options.data.push(this.NormalizeAddress(series, sheet_source));
910
+ }
911
+ }
912
+ }
913
+ */
914
+ }
915
+
916
+ if (type !== 'scatter2') {
917
+ if (parse_result.expression.args[1] && parse_result.expression.args[1].type === 'range') {
918
+ options.labels = this.NormalizeAddress(parse_result.expression.args[1], sheet_source);
919
+ }
920
+ }
921
+
922
+ if (type === 'scatter'
923
+ && parse_result.expression.args[4]
924
+ && parse_result.expression.args[4].type === 'literal'
925
+ && parse_result.expression.args[4].value.toString().toLowerCase() === 'smooth') {
926
+
927
+ options.smooth = true;
928
+ }
929
+ else if (type === 'scatter2' && parse_result.expression.args[2]) {
930
+ if (parse_result.expression.args[2].type === 'literal'
931
+ && /smooth/i.test(parse_result.expression.args[2].value.toString())) {
932
+ options.smooth = true;
933
+ }
934
+ }
935
+
936
+ if (annotation.rect) {
937
+ charts.push({
938
+ anchor: this.AnnotationRectToAnchor(annotation.rect, sheet_source), options});
939
+ // sheet.AddChart(this.AnnotationRectToAnchor(annotation.rect, sheet_source), options);
940
+ }
941
+ else if (annotation.layout) {
942
+ charts.push({
943
+ anchor: this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options});
944
+ // sheet.AddChart(this.AnnotationLayoutToAnchor(annotation.layout, sheet_source), options);
945
+ }
946
+ else {
947
+ console.warn('annotation missing layout');
948
+ }
949
+
950
+ }
951
+
952
+ }
953
+
954
+
955
+ }
956
+
957
+ return charts;
958
+
959
+ }
960
+
961
+ public FormulaText(text: string, context: Cell): string {
962
+
963
+ // let mared = false;
964
+
965
+ if (text[0] !== '=') {
966
+ return text;
967
+ }
968
+
969
+ const parse_result = this.parser.Parse(text);
970
+
971
+ if (!parse_result.expression) {
972
+ console.warn('parsing function failed');
973
+ console.warn(text);
974
+ return text.substring(1);
975
+ }
976
+ else {
977
+
978
+ // if (this.decorated_functions.length) {
979
+ {
980
+ this.parser.Walk(parse_result.expression, (unit) => {
981
+ if (unit.type === 'call') {
982
+ // unit.name = unit.name.toUpperCase();
983
+
984
+ const lc = unit.name.toLowerCase();
985
+
986
+ /*
987
+ for (const test of this.decorated_functions) {
988
+ if (test === lc) {
989
+ unit.name = '_xlfn.' + unit.name;
990
+ break;
991
+ }
992
+ }
993
+ */
994
+
995
+ if (this.decorated_functions[lc]) {
996
+ // mared = true;
997
+ unit.name = this.decorated_functions[lc] + '.' + unit.name;
998
+ }
999
+
1000
+ }
1001
+ return true;
1002
+ });
1003
+ }
1004
+
1005
+ //if (mared) {
1006
+ // console.info("MARED", this.parser.Render(parse_result.expression, undefined, ''));
1007
+ //}
1008
+
1009
+ // const x = this.parser.Render(parse_result.expression, undefined, '');
1010
+ // console.info("T", text, x);
1011
+
1012
+ const table_name = context.table?.name || '';
1013
+
1014
+ /*
1015
+ console.info('tn', table_name);
1016
+ const temp = this.parser.Render(parse_result.expression, undefined, '', undefined, undefined, undefined, true, table_name);
1017
+ console.info({temp});
1018
+ */
1019
+
1020
+ return this.parser.Render(parse_result.expression, {
1021
+ missing: '',
1022
+ long_structured_references: true,
1023
+ table_name });
1024
+ }
1025
+
1026
+ }
1027
+
1028
+ public async Export(source: {
1029
+ sheet_data: SerializedSheet[];
1030
+ active_sheet?: number;
1031
+ named_ranges?: {[index: string]: IArea};
1032
+ named_expressions?: Array<{ name: string, expression: string }>;
1033
+ decimal_mark: ','|'.';
1034
+ }): Promise<void> {
1035
+
1036
+ // --- create a map --------------------------------------------------------
1037
+
1038
+ // active_sheet, in source, is a sheet ID. we need to map
1039
+ // that to an index. luckily we preserve index order. we can
1040
+ // do that as a side effect of creating the map, although we
1041
+ // will need a loop index.
1042
+
1043
+ let active_sheet = 0;
1044
+
1045
+ const sheet_name_map: string[] = [];
1046
+ for (let index = 0; index < source.sheet_data.length; index++) {
1047
+ const sheet = source.sheet_data[index];
1048
+
1049
+ const id = sheet.id || 0;
1050
+ if (id) {
1051
+ sheet_name_map[id] = sheet.name || '';
1052
+ }
1053
+ if (id === source.active_sheet) {
1054
+ active_sheet = index;
1055
+ }
1056
+ }
1057
+
1058
+ // console.info("active sheet", source.active_sheet, active_sheet);
1059
+
1060
+ // --- init workbook globals -----------------------------------------------
1061
+
1062
+ // shared strings, start empty
1063
+ const shared_strings = new SharedStrings();
1064
+
1065
+ // style and theme: use the template so we have the base values
1066
+ const style_cache = new StyleCache();
1067
+ const theme = new Theme();
1068
+
1069
+ let data = await this.zip?.file('xl/theme/theme1.xml')?.async('text') as string;
1070
+ theme.FromXML(this.xmlparser2.parse(data || ''));
1071
+ // console.info({data, xml: this.xmlparser2.parse(data)})
1072
+
1073
+ data = await this.zip?.file('xl/styles.xml')?.async('text') as string;
1074
+ style_cache.FromXML(this.xmlparser2.parse(data || ''), theme);
1075
+
1076
+ // reset counters
1077
+
1078
+ Drawing.next_drawing_index = 1;
1079
+ Chart.next_chart_index = 1;
1080
+
1081
+ const drawings: Drawing[] = [];
1082
+
1083
+ // we need to keep track of tables in all sheets
1084
+
1085
+ const global_tables: TableDescription[] = [];
1086
+
1087
+ // --- now sheets ----------------------------------------------------------
1088
+
1089
+ for (let sheet_index = 0; sheet_index < source.sheet_data.length; sheet_index++) {
1090
+
1091
+ const sheet = source.sheet_data[sheet_index];
1092
+ const sheet_rels: RelationshipMap = {};
1093
+
1094
+ // FIXME: we could, in theory, type this thing...
1095
+
1096
+ const sheet_attributes = {
1097
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
1098
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
1099
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
1100
+ 'mc:Ignorable': 'x14ac xr xr2 xr3',
1101
+ 'xmlns:x14ac': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac',
1102
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
1103
+ 'xmlns:xr2': 'http://schemas.microsoft.com/office/spreadsheetml/2015/revision2',
1104
+ 'xmlns:xr3': 'http://schemas.microsoft.com/office/spreadsheetml/2016/revision3',
1105
+ 'xr:uid': '{D37933E2-499F-4789-8D13-194E11B743FC}',
1106
+ };
1107
+
1108
+ const default_row_height = sheet.default_row_height ? (sheet.default_row_height / 20 * 15) : 15;
1109
+
1110
+ const dom: any = {
1111
+ worksheet: {
1112
+ a$: {
1113
+ ...sheet_attributes,
1114
+ },
1115
+ dimension: {
1116
+ a$: {
1117
+ ref: 'A1',
1118
+ },
1119
+ },
1120
+ sheetViews: {
1121
+ sheetView: {
1122
+ a$: {
1123
+ // tabSelected: (sheet_index === active_sheet ? 1 : 0),
1124
+ workbookViewId: 0,
1125
+ },
1126
+ },
1127
+ },
1128
+ sheetFormatPr: {
1129
+ a$: default_row_height === 15 ? {
1130
+ 'x14ac:dyDescent': 0.25,
1131
+ } : {
1132
+ defaultRowHeight: default_row_height,
1133
+ customHeight: 1,
1134
+ 'x14ac:dyDescent': 0.25,
1135
+ },
1136
+ },
1137
+ cols: {},
1138
+ sheetData: {},
1139
+ mergeCells: {
1140
+ a$: { count: 0 },
1141
+ },
1142
+ dataValidations: {},
1143
+ hyperlinks: {},
1144
+ pageMargins: {
1145
+ a$: {
1146
+ left: 0.7,
1147
+ right: 0.7,
1148
+ top: 0.75,
1149
+ bottom: 0.75,
1150
+ header: 0.3,
1151
+ footer: 0.3,
1152
+ },
1153
+ },
1154
+ drawing: {},
1155
+ tableParts: {
1156
+ a$: {
1157
+ count: 0,
1158
+ },
1159
+ },
1160
+ extLst: {
1161
+ ext: {
1162
+ a$: {
1163
+ uri: '{05C60535-1F16-4fd2-B633-F4F36F0B64E0}',
1164
+ 'xmlns:x14': 'http://schemas.microsoft.com/office/spreadsheetml/2009/9/main',
1165
+ },
1166
+ },
1167
+ },
1168
+
1169
+ },
1170
+ };
1171
+
1172
+ /*
1173
+ const column_styles = (sheet.column_style as any) || {};
1174
+
1175
+ for (const key of Object.keys(column_styles)) {
1176
+ console.info(key, '=>', column_styles[key]);
1177
+
1178
+ }
1179
+
1180
+ return;
1181
+ */
1182
+
1183
+ // data has different representations. it is either blocked into rows or
1184
+ // columns, or a set of individual cells. we could theoretically guarantee
1185
+ // a particular encoding if we wanted to (by row would be optimal for excel).
1186
+
1187
+ // but we don't do that at the moment, so let's just unwind it using the
1188
+ // standard class (adding support for cell styles)
1189
+
1190
+ const cell_style_refs = sheet.styles || sheet.cell_style_refs || [];
1191
+
1192
+ const cells = new Cells();
1193
+ cells.FromJSON(sheet.data, cell_style_refs);
1194
+
1195
+ // these are cells with style but no contents
1196
+
1197
+ for (const entry of sheet.cell_styles) {
1198
+ const cell = cells.EnsureCell(entry); // cheating
1199
+ if (!cell.style) {
1200
+ cell.style = cell_style_refs[entry.ref];
1201
+ }
1202
+ }
1203
+
1204
+ // start with an extent from (0, 0). we can shift this as necessary.
1205
+
1206
+ const extent: IArea = {
1207
+ start: { row: cells.rows + 1, column: cells.columns + 1, },
1208
+ end: { row: cells.rows + 1, column: cells.columns + 1, }};
1209
+
1210
+ // const FormulaText = (text: string) => (text[0] === '=') ? TranslateFormula(text.substr(1)) : text;
1211
+
1212
+
1213
+ // cells data is row-major, and sparse.
1214
+
1215
+ const sheet_data: any = { row: [] };
1216
+
1217
+ const hyperlinks: Array<{
1218
+ rel: string,
1219
+ target: string,
1220
+ address: ICellAddress,
1221
+ }> = [];
1222
+
1223
+ const sparklines: Array<{
1224
+ address: ICellAddress,
1225
+ formula: string,
1226
+ style?: Style.Properties,
1227
+ }> = [];
1228
+
1229
+ const merges: Area[] = [];
1230
+ const tables: TableDescription[] = [];
1231
+
1232
+ const validations: Array<{
1233
+ address: ICellAddress,
1234
+ validation: DataValidation,
1235
+ }> = [];
1236
+
1237
+ // --
1238
+
1239
+ for (let r = 0; r < cells.data.length; r++ ) {
1240
+ if (cells.data[r] && cells.data[r].length) {
1241
+
1242
+ // push out the extent (reversed)
1243
+ if (r < extent.start.row) {
1244
+ extent.start.row = r;
1245
+ }
1246
+
1247
+ // row span
1248
+ const span = {start: -1, end: -1};
1249
+ const row: any = [];
1250
+
1251
+ for (let c = 0; c < cells.data[r].length; c++) {
1252
+ const cell = cells.data[r][c];
1253
+ if (cell) {
1254
+
1255
+ // create a table reference at the table head, we can ignore the rest
1256
+
1257
+ if (cell.table &&
1258
+ cell.table.area.start.row === r &&
1259
+ cell.table.area.start.column === c) {
1260
+
1261
+ const area = new Area(cell.table.area.start, cell.table.area.end);
1262
+ const global_count = global_tables.length + 1;
1263
+ const path = `../tables/table${global_count}.xml`;
1264
+
1265
+ // column names must match the text in the column. AND, they
1266
+ // have to be unique. case-insensitive unique! we are not (atm)
1267
+ // enforcing those rules, so we need to enforce them on export.
1268
+
1269
+ // also, values (and column headers) MUST BE STRINGS.
1270
+
1271
+ const columns: string[] = [];
1272
+ for (let i = 0; i < area.columns; i++) {
1273
+ const header = cells.data[r][c + i];
1274
+ let value = '';
1275
+
1276
+ if (header.type !== ValueType.string) {
1277
+ if (typeof header.calculated !== 'undefined') {
1278
+ value = (header.calculated).toString();
1279
+ }
1280
+ else if (typeof header.value !== 'undefined') {
1281
+ value = (header.value).toString();
1282
+ }
1283
+
1284
+ header.type = ValueType.string;
1285
+ header.value = value;
1286
+
1287
+ }
1288
+ else {
1289
+ value = (header.value as string) || '';
1290
+ }
1291
+
1292
+ if (!value) {
1293
+ value = `Column${i + 1}`;
1294
+ }
1295
+
1296
+ let proposed = value;
1297
+ let success = false;
1298
+ let index = 1;
1299
+
1300
+ while (!success) {
1301
+ success = true;
1302
+ inner_loop:
1303
+ for (const check of columns) {
1304
+ if (check.toLowerCase() === proposed.toLowerCase()) {
1305
+ success = false;
1306
+ proposed = `${value}${++index}`;
1307
+ break inner_loop;
1308
+ }
1309
+ }
1310
+ }
1311
+
1312
+ header.value = proposed;
1313
+ columns.push(proposed);
1314
+
1315
+ }
1316
+
1317
+ let footers: TableFooterType[]|undefined = undefined;
1318
+
1319
+ if (cell.table.totals_row) {
1320
+
1321
+ footers = [];
1322
+
1323
+ for (let i = 0; i < area.columns; i++) {
1324
+ const footer = cells.data[area.end.row][area.start.column + i];
1325
+ if (footer.type) {
1326
+ if (footer.type === ValueType.formula) {
1327
+ footers[i] = {
1328
+ type: 'formula',
1329
+ value: (footer.value || '').toString().substring(1),
1330
+ }
1331
+ }
1332
+ else {
1333
+
1334
+ if (footer.type !== ValueType.string) {
1335
+ footer.type = ValueType.string;
1336
+ footer.value = footer.value?.toString() || '';
1337
+ }
1338
+
1339
+ footers[i] = {
1340
+ type: 'label',
1341
+ value: footer.value as string,
1342
+ }
1343
+ }
1344
+ }
1345
+ // console.info({footer});
1346
+ }
1347
+ }
1348
+
1349
+ // console.info({columns});
1350
+
1351
+ const description: TableDescription = {
1352
+ rel: AddRel(
1353
+ sheet_rels,
1354
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
1355
+ path,
1356
+ ),
1357
+ index: global_count,
1358
+ ref: area.spreadsheet_label,
1359
+ name: `Table${global_count}`,
1360
+ display_name: `Table${global_count}`,
1361
+ totals_row_shown: 0,
1362
+ totals_row_count: cell.table?.totals_row? 1 : 0,
1363
+ columns,
1364
+ footers,
1365
+ };
1366
+
1367
+ if (cell.table.totals_row) {
1368
+ const filter_area = new Area(area.start, {
1369
+ row: area.end.row - 1,
1370
+ column: area.end.column,
1371
+ });
1372
+ description.filterRef = filter_area.spreadsheet_label;
1373
+ }
1374
+
1375
+ // console.info({description});
1376
+
1377
+ // this list is used to add tables on this sheet
1378
+ tables.push(description);
1379
+
1380
+ // but we also need global references to create the files
1381
+ global_tables.push(description);
1382
+ }
1383
+
1384
+ // merges
1385
+
1386
+ if (cell.merge_area &&
1387
+ cell.merge_area.start.row === r &&
1388
+ cell.merge_area.start.column === c) {
1389
+ merges.push(new Area(cell.merge_area.start, cell.merge_area.end));
1390
+ }
1391
+
1392
+ // links
1393
+
1394
+ if (cell.hyperlink) {
1395
+ const rel = AddRel(sheet_rels,
1396
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
1397
+ cell.hyperlink, 'External');
1398
+ hyperlinks.push({
1399
+ rel, target: cell.hyperlink, address: {row: r, column: c},
1400
+ });
1401
+ }
1402
+
1403
+ if (cell.validation && (cell.validation.type === ValidationType.List || cell.validation.type === ValidationType.Range)) {
1404
+ validations.push({
1405
+ address: {row: r, column: c},
1406
+ validation: cell.validation,
1407
+ });
1408
+ }
1409
+
1410
+ // short-circuit here
1411
+ if (cell.type === ValueType.formula && /^=?sparkline\./i.test(cell.value as string)) {
1412
+ sparklines.push({
1413
+ address: {row: r, column: c},
1414
+ formula: cell.value as string,
1415
+ style: cell.style,
1416
+ })
1417
+ continue;
1418
+ }
1419
+
1420
+ // push out the extent (reversed)
1421
+ if (c < extent.start.column) {
1422
+ extent.start.column = c;
1423
+ }
1424
+
1425
+ // update span: end is implicit
1426
+ if (span.start < 0) {
1427
+ span.start = c;
1428
+ }
1429
+ span.end = c;
1430
+
1431
+ // we have to stack the styles? what if there's no cell style?
1432
+ // there are definitely column styles...
1433
+
1434
+ // s is style, index into the style table
1435
+ const s: number|undefined = this.StyleFromCell(sheet, style_cache, r, c, cell.style);
1436
+
1437
+ // v (child element) is the value
1438
+ let v: CellValue = undefined;
1439
+ let t: string|undefined;
1440
+ let f: any; // string|undefined;
1441
+
1442
+ switch (cell.type) {
1443
+ case ValueType.formula:
1444
+ f = this.FormulaText(cell.value as string, cell);
1445
+ switch (cell.calculated_type) {
1446
+ case ValueType.string:
1447
+ v = cell.calculated;
1448
+ t = 'str';
1449
+ break;
1450
+
1451
+ case ValueType.number:
1452
+ v = cell.calculated;
1453
+ break;
1454
+
1455
+ case ValueType.boolean:
1456
+ v = (cell.calculated ? 1 : 0);
1457
+ t = 'b';
1458
+ break;
1459
+ }
1460
+ break;
1461
+
1462
+ case ValueType.string:
1463
+ v = shared_strings.Ensure(cell.value as string);
1464
+ t = 's'; // shared string
1465
+ break;
1466
+
1467
+ case ValueType.number:
1468
+ v = cell.value;
1469
+ break;
1470
+
1471
+ case ValueType.boolean:
1472
+ v = (cell.value ? 1 : 0);
1473
+ t = 'b';
1474
+ break;
1475
+
1476
+ //default:
1477
+ // v = 0;
1478
+ }
1479
+
1480
+ if (cell.area && cell.area.start.row === r && cell.area.start.column === c) {
1481
+ if (typeof f === 'string') {
1482
+ f = {
1483
+ t$: f,
1484
+ a$: {
1485
+ t: 'array',
1486
+ ref: cell.area.spreadsheet_label,
1487
+ },
1488
+ }
1489
+ }
1490
+ }
1491
+
1492
+ // zerp
1493
+ const element: any = {
1494
+ a$: {
1495
+ r: Area.CellAddressToLabel({row: r, column: c}),
1496
+ // t,
1497
+ // s,
1498
+ },
1499
+ // v,
1500
+ };
1501
+
1502
+ if (t !== undefined) {
1503
+ element.a$.t = t;
1504
+ }
1505
+ if (s !== undefined) {
1506
+ element.a$.s = s;
1507
+ }
1508
+ if (f !== undefined) {
1509
+ element.f = f;
1510
+ }
1511
+ if (v !== undefined) {
1512
+ element.v = v;
1513
+ }
1514
+
1515
+ row.push(element);
1516
+
1517
+ }
1518
+ }
1519
+
1520
+ if (row.length) {
1521
+ const row_data: any = {
1522
+ a$: {
1523
+ r: r + 1,
1524
+ spans: `${span.start + 1}:${span.end + 1}`,
1525
+ },
1526
+ c: row,
1527
+ };
1528
+ if (sheet.row_height
1529
+ && (typeof sheet.row_height[r] === 'number')
1530
+ && sheet.row_height[r] !== sheet.default_row_height) {
1531
+
1532
+ row_data.a$.customHeight = 1;
1533
+ row_data.a$.ht = sheet.row_height[r] * 3 / 4;
1534
+ }
1535
+
1536
+ sheet_data.row.push(row_data);
1537
+ }
1538
+
1539
+ }
1540
+ }
1541
+
1542
+ // --- cols ----------------------------------------------------------------
1543
+
1544
+ // the "cols" element represents column styles and nonstandard column
1545
+ // widths. FIXME: should we put sheet style in here as well? I think so...
1546
+
1547
+ const column_entries: Array<{
1548
+ style?: number;
1549
+ width?: number;
1550
+ index: number;
1551
+ }> = [];
1552
+
1553
+ if (sheet.default_column_width) {
1554
+ dom.worksheet.sheetFormatPr.a$.defaultColWidth = // sheet.default_column_width * one_hundred_pixels / 100;
1555
+ PixelsToColumnWidth(sheet.default_column_width);
1556
+ }
1557
+
1558
+ for (let c = 0; c < sheet.columns; c++) {
1559
+ const entry: { style?: number, width?: number, index: number } = { index: c };
1560
+ if (sheet.column_width
1561
+ && sheet.default_column_width
1562
+ && (typeof sheet.column_width[c] === 'number')
1563
+ && sheet.column_width[c] !== sheet.default_column_width) {
1564
+
1565
+ entry.width = // sheet.column_width[c] * one_hundred_pixels / 100;
1566
+ PixelsToColumnWidth(sheet.column_width[c]);
1567
+
1568
+ // console.info("COLUMN", c, 'width', sheet.column_width[c], 'calc?', entry.width, '100p', one_hundred_pixels);
1569
+
1570
+ }
1571
+
1572
+ let style = sheet.column_style[c];
1573
+ if (typeof style === 'number') {
1574
+ style = cell_style_refs[style];
1575
+ if (style) {
1576
+ entry.style = style_cache.EnsureStyle(style_cache.StyleOptionsFromProperties(style));
1577
+ }
1578
+ }
1579
+ else if (style) {
1580
+ entry.style = style_cache.EnsureStyle(style_cache.StyleOptionsFromProperties(style));
1581
+ }
1582
+
1583
+ //if (sheet.column_style[c]) {
1584
+ // entry.style = style_cache.EnsureStyle(style_cache.StyleOptionsFromProperties(sheet.column_style[c]));
1585
+ //}
1586
+
1587
+ if (entry.style !== undefined || entry.width !== undefined) {
1588
+ column_entries[c] = entry;
1589
+ }
1590
+ }
1591
+
1592
+ // we're short-cutting here, these should be arranged in blocks if
1593
+ // there's overlap. not sure how much of an issue that is though.
1594
+
1595
+ if (column_entries.length) {
1596
+ for (const entry of column_entries) {
1597
+ dom.worksheet.cols.col = column_entries.map(entry => {
1598
+ const a$: any = {
1599
+ min: entry.index + 1,
1600
+ max: entry.index + 1,
1601
+ };
1602
+ if (entry.style !== undefined) {
1603
+ a$.style = entry.style;
1604
+ }
1605
+ if (entry.width !== undefined) {
1606
+ a$.width = entry.width;
1607
+ a$.customWidth = 1;
1608
+ }
1609
+ else {
1610
+ a$.width = // (sheet.default_column_width || 100) / 100 * one_hundred_pixels;
1611
+ PixelsToColumnWidth(sheet.default_column_width || 90);
1612
+ }
1613
+ return {a$};
1614
+ });
1615
+ }
1616
+ }
1617
+ else {
1618
+ delete dom.worksheet.cols;
1619
+ }
1620
+
1621
+ // --- validation ----------------------------------------------------------
1622
+
1623
+ if (validations.length) {
1624
+
1625
+ dom.worksheet.dataValidations = {
1626
+ a$: { count: validations.length },
1627
+ dataValidation: validations.map(validation => {
1628
+ const entry: any = {
1629
+ a$: {
1630
+ type: 'list',
1631
+ allowBlank: 1,
1632
+ showInputMessage: 1,
1633
+ showErrorMessage: 1,
1634
+ sqref: new Area(validation.address).spreadsheet_label,
1635
+ },
1636
+ };
1637
+ if (validation.validation.type === ValidationType.Range) {
1638
+
1639
+ const range: UnitRange = {
1640
+ id: 0,
1641
+ type: 'range',
1642
+ label: '', position: 0,
1643
+ start:
1644
+ {...validation.validation.area.start, absolute_column: true, absolute_row: true, id: 0, label: '', position: 0, type: 'address', },
1645
+ end:
1646
+ {...validation.validation.area.end, absolute_column: true, absolute_row: true, id: 0, label: '', position: 0, type: 'address', }
1647
+ ,
1648
+ }
1649
+
1650
+ if (typeof validation.validation.area.start.sheet_id !== 'undefined') {
1651
+ range.start.sheet = sheet_name_map[validation.validation.area.start.sheet_id];
1652
+ }
1653
+
1654
+ /*
1655
+ const area = new Area(
1656
+ {...validation.validation.area.start, absolute_column: true, absolute_row: true},
1657
+ {...validation.validation.area.end, absolute_column: true, absolute_row: true},
1658
+ );
1659
+
1660
+ entry.formula1 = `${area.spreadsheet_label}`;
1661
+ */
1662
+ entry.formula1 = this.parser.Render(range);
1663
+
1664
+ }
1665
+ else if (validation.validation.type === ValidationType.List) {
1666
+ entry.formula1 = `"${validation.validation.list.join(',')}"`;
1667
+ }
1668
+ return entry;
1669
+ }),
1670
+ };
1671
+
1672
+ }
1673
+ else {
1674
+ delete dom.worksheet.dataValidations;
1675
+ }
1676
+
1677
+ // --- tables ------------------------------------------------------------
1678
+
1679
+ if (tables.length) {
1680
+ dom.worksheet.tableParts.a$.count = tables.length;
1681
+ dom.worksheet.tableParts.tablePart = tables.map(table => {
1682
+ return {
1683
+ a$: {
1684
+ 'r:id': table.rel || '',
1685
+ }
1686
+ };
1687
+ });
1688
+ }
1689
+ else {
1690
+ delete dom.worksheet.tableParts;
1691
+ }
1692
+
1693
+ // const GUID = () => '{' + uuidv4().toUpperCase() + '}';
1694
+
1695
+ for (const table of tables) {
1696
+
1697
+ const totals_attributes: { totalsRowCount?: number } = {};
1698
+ if (table.totals_row_count) {
1699
+ totals_attributes.totalsRowCount = 1;
1700
+ }
1701
+
1702
+ const tableColumns: any = {
1703
+ a$: {
1704
+ count: (table.columns || []).length,
1705
+ },
1706
+ tableColumn: [],
1707
+ /*
1708
+ tableColumn: (table.columns||[]).map((column, index) => ({
1709
+ a$: {
1710
+ id: index + 1,
1711
+ // 'xr3:uid': GUID(),
1712
+ name: column || ('Column' + (index + 1)),
1713
+ },
1714
+ })),
1715
+ */
1716
+ };
1717
+
1718
+ if (table.columns) {
1719
+ for (let i = 0; i < table.columns.length; i++) {
1720
+ const column = table.columns[i];
1721
+ const footer = (table.footers || [])[i];
1722
+ const obj: any = {
1723
+ a$: {
1724
+ id: i + 1,
1725
+ name: column || `Column${i + 1}`,
1726
+ },
1727
+ };
1728
+
1729
+ if (footer) {
1730
+ if (footer.type === 'label') {
1731
+ obj.a$.totalsRowLabel = footer.value;
1732
+ }
1733
+ else if (footer.type === 'formula') {
1734
+ obj.a$.totalsRowFunction = 'custom';
1735
+ obj.totalsRowFormula = footer.value;
1736
+ }
1737
+ }
1738
+
1739
+ tableColumns.tableColumn.push(obj);
1740
+
1741
+ }
1742
+ }
1743
+
1744
+ const table_dom = {
1745
+ table: {
1746
+ a$: {
1747
+ xmlns: 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
1748
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
1749
+ 'mc:Ignorable': 'xr xr3',
1750
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
1751
+ 'xmlns:xr3': 'http://schemas.microsoft.com/office/spreadsheetml/2016/revision3',
1752
+ id: table.index || 0,
1753
+ // 'xr:uid': '{676B775D-AA84-41B6-8450-8515A94D2D7B}',
1754
+ name: table.name,
1755
+ displayName: table.display_name,
1756
+ ...totals_attributes,
1757
+ ref: table.ref,
1758
+ // 'xr:uid': GUID(),
1759
+ },
1760
+
1761
+ autoFilter: {
1762
+ a$: {
1763
+ ref: table.filterRef || table.ref,
1764
+ // 'xr:uid': GUID(),
1765
+ },
1766
+ },
1767
+
1768
+ tableColumns,
1769
+
1770
+ tableStyleInfo: {
1771
+ a$: {
1772
+ name: 'TableStyleMedium2',
1773
+ showFirstColumn: 0,
1774
+ showLastColumn: 0,
1775
+ showRowStripes: 1,
1776
+ showColumnStripes: 0,
1777
+ },
1778
+ },
1779
+
1780
+ },
1781
+ };
1782
+
1783
+ const xml = XMLDeclaration + this.xmlbuilder1.build(table_dom);
1784
+ await this.zip?.file(`xl/tables/table${table.index}.xml`, xml);
1785
+
1786
+ }
1787
+
1788
+ // --- merges ------------------------------------------------------------
1789
+
1790
+ if (merges.length) {
1791
+ dom.worksheet.mergeCells.a$.count = merges.length;
1792
+ dom.worksheet.mergeCells.mergeCell = merges.map(merge => {
1793
+ return {
1794
+ a$: { ref: merge.spreadsheet_label }
1795
+ };
1796
+ });
1797
+ }
1798
+ else {
1799
+ delete dom.worksheet.mergeCells;
1800
+ }
1801
+
1802
+ // --- hyperlinks --------------------------------------------------------
1803
+
1804
+ if (hyperlinks.length) {
1805
+ dom.worksheet.hyperlinks.hyperlink = hyperlinks.map(link => {
1806
+ return {
1807
+ a$: {
1808
+ 'r:id': link.rel,
1809
+ ref: new Area(link.address).spreadsheet_label,
1810
+ 'xr:uid': '{0C6B7792-7EA0-4932-BF15-D49C453C565D}',
1811
+ },
1812
+ };
1813
+ });
1814
+ }
1815
+ else {
1816
+ delete dom.worksheet.hyperlinks;
1817
+ }
1818
+
1819
+ // --- sparklines --------------------------------------------------------
1820
+
1821
+ if (sparklines.length) {
1822
+ dom.worksheet.extLst.ext['x14:sparklineGroups'] = {
1823
+ a$: {
1824
+ 'xmlns:xm': 'http://schemas.microsoft.com/office/excel/2006/main',
1825
+ },
1826
+ 'x14:sparklineGroup': sparklines.map(sparkline => {
1827
+
1828
+ const result = this.parser.Parse(sparkline.formula);
1829
+ let source = '';
1830
+ if (result.expression
1831
+ && result.expression.type === 'call'
1832
+ && result.expression.args.length > 0) {
1833
+ const arg = result.expression.args[0];
1834
+ if (arg.type === 'range' || arg.type === 'address') {
1835
+
1836
+ const start = (arg.type === 'range') ? arg.start : arg;
1837
+ if (!start.sheet) {
1838
+ if (typeof start.sheet_id !== 'undefined') {
1839
+ start.sheet = sheet_name_map[start.sheet_id];
1840
+ }
1841
+ else {
1842
+ start.sheet = sheet.name;
1843
+ }
1844
+ }
1845
+ source = this.parser.Render(arg);
1846
+
1847
+ }
1848
+ }
1849
+
1850
+ const a$: any = {
1851
+ displayEmptyCellsAs: 'gap',
1852
+ };
1853
+
1854
+ if (/column/i.test(sparkline.formula)) {
1855
+ a$.type = 'column';
1856
+ }
1857
+
1858
+ return {
1859
+ a$,
1860
+ 'x14:colorSeries': { a$: { rgb: 'FF376092' }},
1861
+ 'x14:sparklines': {
1862
+ 'x14:sparkline': {
1863
+ 'xm:f': source,
1864
+ 'xm:sqref': new Area(sparkline.address).spreadsheet_label,
1865
+ },
1866
+ },
1867
+ }
1868
+ }),
1869
+ };
1870
+ }
1871
+ else {
1872
+ delete dom.worksheet.extLst;
1873
+ }
1874
+
1875
+ dom.worksheet.sheetData = sheet_data;
1876
+
1877
+ // --- charts ------------------------------------------------------------
1878
+
1879
+ const charts = this.ParseCharts(sheet);
1880
+ const images = this.ParseImages(sheet);
1881
+
1882
+ // if a sheet has one or more charts, it has a single drawing. for a
1883
+ // drawing, we need
1884
+ //
1885
+ // (1) entry in sheet xml
1886
+ // (2) drawing xml file
1887
+ // (3) relationship sheet -> drawing
1888
+ // (4) drawing rels file (for charts, later)
1889
+ // (5) entry in [ContentTypes]
1890
+ //
1891
+ // each chart in the drawing then needs
1892
+ //
1893
+ // (1) entry in drawing file
1894
+ // (2) chart xml file
1895
+ // (3) relationship drawing -> chart
1896
+ // (4) chart/colors xml file
1897
+ // (5) chart/style xml file
1898
+ // (6) chart rels file
1899
+ // (7) relationship chart -> colors
1900
+ // (8) relationship chart -> style
1901
+ // (9) entry in [ContentTypes]
1902
+ //
1903
+ // check: we can get away with not including colors or style, which
1904
+ // will revert to defaults -- let's do that for the time being if we can
1905
+ //
1906
+ // merging in images, which use the same drawing (and in a single
1907
+ // sheet, a single drawing holds both charts and images).
1908
+
1909
+ if (charts.length || images.length) {
1910
+
1911
+ const drawing = new Drawing();
1912
+
1913
+ for (const chart of charts) {
1914
+ drawing.AddChart(chart.options, chart.anchor);
1915
+ }
1916
+
1917
+ for (const image of images) {
1918
+ drawing.AddImage(image.options, image.anchor);
1919
+ }
1920
+
1921
+ for (const {image} of drawing.images) {
1922
+ // console.info({image}, `xl/media/image${image.index}.${image.extension}`);
1923
+ await this.zip?.file(`xl/media/image${image.index}.${image.extension}`, image.options.data||'', {
1924
+ base64: image.options.encoding === 'base64'
1925
+ });
1926
+ // no media rels!
1927
+ }
1928
+
1929
+ for (const {chart} of drawing.charts) {
1930
+ const dom = chart.toJSON();
1931
+ const xml = XMLDeclaration + this.xmlbuilder1.build(dom);
1932
+ await this.zip?.file(`xl/charts/chart${chart.index}.xml`, xml);
1933
+ await this.WriteRels(chart.relationships, `xl/charts/_rels/chart${chart.index}.xml.rels`);
1934
+ }
1935
+
1936
+ await this.WriteRels(drawing.relationships, `xl/drawings/_rels/drawing${drawing.index}.xml.rels`);
1937
+
1938
+ const xml = XMLDeclaration + this.xmlbuilder1.build(drawing.toJSON());
1939
+
1940
+ await this.zip?.file(`xl/drawings/drawing${drawing.index}.xml`, xml);
1941
+
1942
+ drawings.push(drawing); // for [ContentTypes]
1943
+
1944
+ const drawing_rel = AddRel(sheet_rels,
1945
+ `http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing`,
1946
+ `../drawings/drawing${drawing.index}.xml`);
1947
+
1948
+ dom.worksheet.drawing = {
1949
+ a$: {
1950
+ 'r:id': drawing_rel,
1951
+ },
1952
+ };
1953
+
1954
+ }
1955
+ else {
1956
+ delete dom.worksheet.drawing;
1957
+ }
1958
+
1959
+ // --- end? --------------------------------------------------------------
1960
+
1961
+ // it seems like chrome, at least, will maintain order. but this is
1962
+ // not gauranteed and we can't rely on it. the best thing to do might
1963
+ // be to use the renderer on blocks and then assemble the blocks ourselves.
1964
+
1965
+ dom.worksheet.dimension.a$.ref = new Area(extent.start, extent.end).spreadsheet_label;
1966
+ const xml = XMLDeclaration + this.xmlbuilder1.build(dom);
1967
+
1968
+ // write this into the file
1969
+
1970
+ await this.zip?.file(`xl/worksheets/sheet${sheet_index + 1}.xml`, xml);
1971
+ if (Object.keys(sheet_rels).length) {
1972
+ this.WriteRels(sheet_rels, `xl/worksheets/_rels/sheet${sheet_index + 1}.xml.rels`);
1973
+ }
1974
+
1975
+ }
1976
+
1977
+
1978
+
1979
+ // these are workbook global so after all sheets are done
1980
+
1981
+ await this.WriteSharedStrings(shared_strings);
1982
+ await this.WriteStyleCache(style_cache);
1983
+
1984
+ // now have to write/update
1985
+ //
1986
+ // (1) contentTypes
1987
+ // (2) workbook.xml
1988
+ // (3) workbook.xml.rels
1989
+ //
1990
+
1991
+ const workbook_rels: RelationshipMap = {};
1992
+ AddRel(workbook_rels, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles', 'styles.xml');
1993
+ AddRel(workbook_rels, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme', 'theme/theme1.xml');
1994
+ AddRel(workbook_rels, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings', 'sharedStrings.xml');
1995
+
1996
+ const worksheet_rels_map = source.sheet_data.map((sheet, index) => AddRel(
1997
+ workbook_rels, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet',
1998
+ `worksheets/sheet${index + 1}.xml`,
1999
+ ));
2000
+
2001
+ await this.WriteRels(workbook_rels, `xl/_rels/workbook.xml.rels`);
2002
+
2003
+ let definedNames: any = {definedName: []};
2004
+ if (source.named_ranges) {
2005
+ const keys = Object.keys(source.named_ranges);
2006
+ for (const key of keys) {
2007
+ let sheet_name = '';
2008
+ const area = new Area(source.named_ranges[key].start, source.named_ranges[key].end);
2009
+ area.start.absolute_column = area.start.absolute_row = true;
2010
+ area.end.absolute_column = area.end.absolute_row = true;
2011
+
2012
+ if (area.start.sheet_id) {
2013
+ for (const sheet of source.sheet_data) {
2014
+ if (sheet.id === area.start.sheet_id) {
2015
+ sheet_name = sheet.name || '';
2016
+ break;
2017
+ }
2018
+ }
2019
+ }
2020
+
2021
+ if (sheet_name) {
2022
+ if (QuotedSheetNameRegex.test(sheet_name)) {
2023
+ sheet_name = `'${sheet_name}'`;
2024
+ }
2025
+ sheet_name += '!';
2026
+ }
2027
+
2028
+ // console.info({key, area, lx: area.spreadsheet_label, sheet_name });
2029
+ definedNames.definedName.push({
2030
+ a$: { name: key },
2031
+ t$: sheet_name + area.spreadsheet_label,
2032
+ });
2033
+
2034
+ }
2035
+ }
2036
+ if (source.named_expressions) {
2037
+ for (const entry of source.named_expressions) {
2038
+ definedNames.definedName.push({
2039
+ a$: { name: entry.name },
2040
+ t$: entry.expression,
2041
+ });
2042
+ }
2043
+ }
2044
+ if (!definedNames.definedName.length) {
2045
+ definedNames = undefined;
2046
+ }
2047
+
2048
+ const workbook_dom: any = {
2049
+ workbook: {
2050
+ a$: {
2051
+ 'xmlns': 'http://schemas.openxmlformats.org/spreadsheetml/2006/main',
2052
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
2053
+ 'xmlns:mc': 'http://schemas.openxmlformats.org/markup-compatibility/2006',
2054
+ 'mc:Ignorable': 'x15 xr xr6 xr10 xr2',
2055
+ 'xmlns:x15': 'http://schemas.microsoft.com/office/spreadsheetml/2010/11/main',
2056
+ 'xmlns:xr': 'http://schemas.microsoft.com/office/spreadsheetml/2014/revision',
2057
+ 'xmlns:xr6': 'http://schemas.microsoft.com/office/spreadsheetml/2016/revision6',
2058
+ 'xmlns:xr10': 'http://schemas.microsoft.com/office/spreadsheetml/2016/revision10',
2059
+ 'xmlns:xr2': 'http://schemas.microsoft.com/office/spreadsheetml/2015/revision2',
2060
+ },
2061
+ workbookPr: {
2062
+ a$: {
2063
+ defaultThemeVersion: '166925',
2064
+ },
2065
+ },
2066
+ bookViews: {
2067
+ workbookView: {
2068
+ a$: {
2069
+ activeTab: (active_sheet || 0),
2070
+ },
2071
+ },
2072
+ },
2073
+ sheets: {
2074
+ sheet: source.sheet_data.map((sheet, index) => {
2075
+ const a$: any = {
2076
+ name: sheet.name || `Sheet${index + 1}`,
2077
+ sheetId: index + 1,
2078
+ 'r:id': worksheet_rels_map[index],
2079
+ };
2080
+ if (sheet.visible === false) {
2081
+ a$.state = 'hidden';
2082
+ }
2083
+ return { a$ };
2084
+ }),
2085
+ },
2086
+ definedNames,
2087
+ },
2088
+ };
2089
+
2090
+ const workbook_xml = XMLDeclaration + this.xmlbuilder1.build(workbook_dom);
2091
+ // console.info(workbook_xml);
2092
+ await this.zip?.file(`xl/workbook.xml`, workbook_xml);
2093
+
2094
+ // const extensions: Array<{ Extension: string, ContentType: string }> = [];
2095
+ const extensions: Record<string, string> = {};
2096
+ for (const drawing of drawings) {
2097
+ for (const image of drawing.images) {
2098
+ switch (image.image.extension) {
2099
+ case 'gif':
2100
+ case 'png':
2101
+ case 'jpeg':
2102
+ extensions[image.image.extension] = 'image/' + image.image.extension;
2103
+ break;
2104
+
2105
+ case 'svg':
2106
+ extensions['svg'] = 'image/svg+xml';
2107
+ break;
2108
+ }
2109
+ }
2110
+ }
2111
+
2112
+ const content_types_dom: any = {
2113
+ Types: {
2114
+ a$: {
2115
+ 'xmlns': 'http://schemas.openxmlformats.org/package/2006/content-types',
2116
+ },
2117
+ Default: [
2118
+ {a$: { Extension: 'rels', ContentType: 'application/vnd.openxmlformats-package.relationships+xml' }},
2119
+ {a$: { Extension: 'xml', ContentType: 'application/xml' }},
2120
+ ...Object.keys(extensions).map(key => ({
2121
+ a$: { Extension: key, ContentType: extensions[key] },
2122
+ })),
2123
+ ],
2124
+ Override: [
2125
+ { a$: { PartName: '/xl/workbook.xml', ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml' }},
2126
+
2127
+ // sheets
2128
+ ...source.sheet_data.map((sheet, index) => {
2129
+ return { a$: {
2130
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml',
2131
+ PartName: `/xl/worksheets/sheet${index + 1}.xml`,
2132
+ }};
2133
+ }),
2134
+
2135
+ // charts and drawings
2136
+ ...drawings.reduce((a: any, drawing) => {
2137
+ return a.concat([
2138
+ ...drawing.charts.map(chart => {
2139
+ return { a$: {
2140
+ ContentType: 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml',
2141
+ PartName: `/xl/charts/chart${chart.chart.index}.xml`,
2142
+ }};
2143
+ }),
2144
+ { a$: {
2145
+ ContentType: 'application/vnd.openxmlformats-officedocument.drawing+xml',
2146
+ PartName: `/xl/drawings/drawing${drawing.index}.xml`,
2147
+ }},
2148
+ ]);
2149
+ }, []),
2150
+
2151
+ { a$: { PartName: '/xl/theme/theme1.xml', ContentType: 'application/vnd.openxmlformats-officedocument.theme+xml' }},
2152
+ { a$: { PartName: '/xl/styles.xml', ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml' }},
2153
+ { a$: { PartName: '/xl/sharedStrings.xml', ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml' }},
2154
+
2155
+ // tables
2156
+ ...global_tables.map(table => {
2157
+ return { a$: {
2158
+ ContentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml',
2159
+ PartName: `/xl/tables/table${table.index || 0}.xml`,
2160
+ }};
2161
+ }),
2162
+
2163
+ { a$: { PartName: '/docProps/core.xml', ContentType: 'application/vnd.openxmlformats-package.core-properties+xml' }},
2164
+ { a$: { PartName: '/docProps/app.xml', ContentType: 'application/vnd.openxmlformats-officedocument.extended-properties+xml' }},
2165
+
2166
+ ],
2167
+ },
2168
+ };
2169
+
2170
+ const content_types_xml = XMLDeclaration + this.xmlbuilder1.build(content_types_dom);
2171
+ // console.info(content_types_xml);
2172
+ await this.zip?.file(`[Content_Types].xml`, content_types_xml);
2173
+
2174
+ }
2175
+
2176
+ /** zip -> binary string */
2177
+ public async AsBinaryString(compression_level?: number) {
2178
+ if (!this.zip) {
2179
+ throw new Error('missing zip');
2180
+ }
2181
+ const opts: JSZip.JSZipGeneratorOptions = { type: 'binarystring' };
2182
+ if (typeof compression_level !== 'undefined') {
2183
+ opts.compression = 'DEFLATE';
2184
+ opts.compressionOptions = {level: compression_level };
2185
+ }
2186
+ const output = await this.zip.generateAsync(opts);
2187
+ return output;
2188
+ }
2189
+
2190
+ /** zip -> blob */
2191
+ public async AsBlob(compression_level?: number) {
2192
+ if (!this.zip) {
2193
+ throw new Error('missing zip');
2194
+ }
2195
+ const opts: JSZip.JSZipGeneratorOptions = { type: 'blob' };
2196
+ if (typeof compression_level !== 'undefined') {
2197
+ opts.compression = 'DEFLATE';
2198
+ opts.compressionOptions = {level: compression_level };
2199
+ }
2200
+ const output = await this.zip.generateAsync(opts);
2201
+ return output;
2202
+ }
2203
+
2204
+ }