@trebco/treb 32.5.0 → 32.6.4

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.
@@ -0,0 +1,187 @@
1
+ import { XMLUtils } from './xml-utils';
2
+
3
+ /**
4
+ * https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.metadatarecord?view=openxml-3.0.1
5
+ *
6
+ * ah, mixing 0-based and 1-based indexes. that's great. not at all confusing.
7
+ */
8
+ export interface MetadataRecord {
9
+
10
+ //
11
+ // A 1-based index to the metadata record type in metadataTypes.
12
+ //
13
+ t: number;
14
+
15
+ //
16
+ // A zero based index to a specific metadata record. If the corresponding
17
+ // metadataType has name="XLMDX", then this is an index to a record in
18
+ // mdxMetadata, otherwise this is an index to a record in the futureMetadata
19
+ // section whose name matches the name of the metadataType.
20
+ //
21
+ v: number;
22
+
23
+ }
24
+
25
+ /**
26
+ * https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.metadatatype?view=openxml-3.0.1
27
+ *
28
+ * punting on all these attributes for now, except the ones we specifically
29
+ * want.
30
+ */
31
+ export interface MetadataType {
32
+
33
+ //
34
+ // Represents the name of this particular metadata type. This name shall be
35
+ // unique amongst all other metadataTypes.
36
+ //
37
+ name: string;
38
+
39
+ //
40
+ // A Boolean flag indicating whether metadata is cell metadata. True when
41
+ // the metadata is cell metadata, false otherwise - in the false case it
42
+ // is considered to be value metadata.
43
+ //
44
+ cell_meta?: boolean;
45
+
46
+ }
47
+
48
+ /**
49
+ * the type `Future Feature Data Storage Area` is not actually defined? not
50
+ * sure. maybe it's just a standard extension type and I need to look that up?
51
+ *
52
+ * for the time being we'll use a list of flags...
53
+ */
54
+ export interface MetadataFlags {
55
+ 'dynamic-array'?: boolean;
56
+ }
57
+
58
+ /**
59
+ * https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.futuremetadata?view=openxml-3.0.1
60
+ * https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.futuremetadatablock?view=openxml-3.0.1
61
+ *
62
+ * these are named, matching the names in "MetadataType"; we'll use those
63
+ * as indexes, and not store them in the object.
64
+ *
65
+ */
66
+ export interface FutureMetadata {
67
+ flags: MetadataFlags;
68
+ }
69
+
70
+ /**
71
+ * we're just reflecting the actual data at this point, not simplifying
72
+ * (although we should simplify). will require some lookups.
73
+ */
74
+ export interface Metadata {
75
+ cell_metadata: MetadataRecord[];
76
+ metadata_types: MetadataType[];
77
+
78
+ // TODO
79
+ // value_metadata: MetadataRecord[];
80
+
81
+ future_metadata: Record<string, FutureMetadata[]>;
82
+
83
+ }
84
+
85
+ export const LookupMetadata = (source: Metadata, type: 'cell'|'value', index: number): FutureMetadata => {
86
+
87
+ if (type === 'cell') {
88
+
89
+ //
90
+ // the docs say "zero based", but this looks to be one based -- there's
91
+ // only one entry when we create a doc, but the cm index in cells is "1".
92
+ //
93
+ // https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cell?view=openxml-3.0.1&redirectedfrom=MSDN
94
+ //
95
+
96
+ const metadata_type = source.cell_metadata[index - 1];
97
+
98
+ if (metadata_type) {
99
+ const record_type = source.metadata_types[metadata_type.t - 1]; // 1-based
100
+ if (record_type) {
101
+ if (record_type.name === 'XLMDX') {
102
+ // ...
103
+ }
104
+ else {
105
+ const future_metadata_list = source.future_metadata[record_type.name];
106
+ if (future_metadata_list) {
107
+ return future_metadata_list[metadata_type.v] || {}; // 0-based
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ else {
114
+ console.warn('value metadata not implemented')
115
+ }
116
+
117
+ return {flags: {}}; // null, essentially
118
+
119
+ };
120
+
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ export const ParseMetadataXML = (xml: any): Metadata => {
123
+
124
+ const metadata: Metadata = {
125
+ metadata_types: [],
126
+ cell_metadata: [],
127
+ future_metadata: {},
128
+ };
129
+
130
+ const metadata_types = XMLUtils.FindAll(xml, 'metadata/metadataTypes/metadataType');
131
+ for (const entry of metadata_types) {
132
+
133
+ const name: string = entry.a$?.name || '';
134
+ const value = entry.a$?.cellMeta;
135
+ const cell_meta: boolean = (value === '1' || value === 'true');
136
+
137
+ metadata.metadata_types.push({
138
+ name, cell_meta
139
+ });
140
+
141
+ }
142
+
143
+ const future_metadata_blocks = XMLUtils.FindAll(xml, 'metadata/futureMetadata');
144
+ for (const entry of future_metadata_blocks) {
145
+
146
+ const name: string = entry.a$?.name || '';
147
+ if (name) {
148
+
149
+ const future_metadata_list: FutureMetadata[] = [];
150
+
151
+ // `extLst` entries can be inside of `bk` entries, but aparently not
152
+ // required? what's the case where they are _not_ in `bk` elements?
153
+
154
+ for (const block of XMLUtils.FindAll(entry, 'bk')) {
155
+
156
+ const future_metadata: FutureMetadata = { flags: {} };
157
+
158
+ // I guess metadata attributes are elements? we'll probably
159
+ // have to look them up individually
160
+
161
+ for (const child of XMLUtils.FindAll(block, 'extLst/ext/xda:dynamicArrayProperties')) {
162
+ if (child?.a$.fDynamic === '1') {
163
+ future_metadata.flags['dynamic-array'] = true;
164
+ }
165
+ }
166
+
167
+ future_metadata_list.push(future_metadata);
168
+
169
+ }
170
+
171
+ metadata.future_metadata[name] = future_metadata_list;
172
+
173
+ }
174
+ }
175
+
176
+ for (const entry of XMLUtils.FindAll(xml, 'metadata/cellMetadata/bk/rc')) {
177
+ metadata.cell_metadata.push({
178
+ t: Number(entry.a$?.t || -1),
179
+ v: Number(entry.a$?.v || -1),
180
+ });
181
+ }
182
+
183
+ // console.info({metadata});
184
+
185
+ return metadata;
186
+
187
+ }
@@ -38,6 +38,7 @@ import type { RelationshipMap } from './relationship';
38
38
  import { ZipWrapper } from './zip-wrapper';
39
39
  import type { CellStyle, ThemeColor } from 'treb-base-types';
40
40
  import type { SerializedNamed } from 'treb-data-model';
41
+ import { type Metadata, ParseMetadataXML } from './metadata';
41
42
 
42
43
  /**
43
44
  * @privateRemarks -- FIXME: not sure about the equal/equals thing. need to check.
@@ -53,20 +54,42 @@ export const ConditionalFormatOperators: Record<string, string> = {
53
54
  notEqual: '<>',
54
55
  };
55
56
 
57
+ //
58
+ // enums? really? in 2025? FIXME
59
+ //
56
60
  export enum ChartType {
57
- Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble, Box
61
+ Null = 0,
62
+ Column,
63
+ Bar,
64
+ Line,
65
+ Scatter,
66
+ Donut,
67
+ Pie,
68
+ Bubble,
69
+ Box,
70
+ Histogram,
71
+ Unknown
58
72
  }
59
73
 
60
74
  export interface ChartSeries {
61
75
  values?: string;
62
76
  categories?: string;
63
77
  bubble_size?: string;
78
+
79
+ /** special for histogram */
80
+ bin_count?: number;
81
+
64
82
  title?: string;
65
83
  }
66
84
 
85
+ type ChartFlags =
86
+ 'stacked'
87
+ ;
88
+
67
89
  export interface ChartDescription {
68
90
  title?: string;
69
91
  type: ChartType;
92
+ flags?: ChartFlags[];
70
93
  series?: ChartSeries[];
71
94
  }
72
95
 
@@ -151,6 +174,9 @@ export class Workbook {
151
174
  /** the workbook "rels" */
152
175
  public rels: RelationshipMap = {};
153
176
 
177
+ /** metadata reference; new and WIP */
178
+ public metadata?: Metadata;
179
+
154
180
  public sheets: Sheet[] = [];
155
181
 
156
182
  public active_tab = 0;
@@ -200,6 +226,13 @@ export class Workbook {
200
226
  let xml = xmlparser2.parse(data || '');
201
227
  this.shared_strings.FromXML(xml);
202
228
 
229
+ // new(ish) metadata
230
+ if (this.zip.Has('xl/metadata.xml')) {
231
+ data = this.zip.Get('xl/metadata.xml');
232
+ xml = xmlparser2.parse(data);
233
+ this.metadata = ParseMetadataXML(xml);
234
+ }
235
+
203
236
  // theme
204
237
  data = this.zip.Get('xl/theme/theme1.xml');
205
238
  xml = xmlparser2.parse(data);
@@ -542,7 +575,7 @@ export class Workbook {
542
575
  const xml = xmlparser2.parse(data);
543
576
 
544
577
  const result: ChartDescription = {
545
- type: ChartType.Unknown
578
+ type: ChartType.Null
546
579
  };
547
580
 
548
581
  // console.info("RC", xml);
@@ -562,8 +595,29 @@ export class Workbook {
562
595
  }
563
596
  }
564
597
  else {
598
+
599
+ // there's a bug in FindAll -- seems to have to do with the nodes
600
+ // here being strings
601
+
602
+ const nodes: (string | { t$: string })[] = [];
603
+ const parents = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r');
604
+ for (const entry of parents) {
605
+ if (entry['a:t']) {
606
+ nodes.push(entry['a:t'])
607
+ }
608
+ }
609
+
610
+ /*
611
+ const xx = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r');
612
+ const yy = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r/a:t');
613
+ console.info({xx, yy});
614
+
565
615
  const nodes = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r/a:t');
566
- result.title = '"' + nodes.join('') + '"';
616
+ */
617
+
618
+ result.title = '"' + nodes.map(node => {
619
+ return typeof node === 'string' ? node : (node.t$ || '');
620
+ }).join('') + '"';
567
621
  }
568
622
 
569
623
  }
@@ -653,6 +707,14 @@ export class Workbook {
653
707
  if (node['c:barDir']) {
654
708
  if (node['c:barDir'].a$?.val === 'col') {
655
709
  result.type = ChartType.Column;
710
+
711
+ if (node['c:grouping']?.a$?.val === 'stacked') {
712
+ if (!result.flags) {
713
+ result.flags = [];
714
+ }
715
+ result.flags.push('stacked');
716
+ }
717
+
656
718
  }
657
719
  }
658
720
 
@@ -705,9 +767,92 @@ export class Workbook {
705
767
  // box plot uses "extended chart" which is totally different... but we
706
768
  // might need it again later? for the time being it's just inlined
707
769
 
770
+ // hmmm also used for histogram... histograms aren't named, they are
771
+ // clustered column type with a binning element, which has some attributes
772
+
708
773
  const ex_series = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:plotArea/cx:plotAreaRegion/cx:series');
709
774
  if (ex_series?.length) {
710
- if (ex_series.every(test => test.a$?.layoutId === 'boxWhisker')) {
775
+
776
+ // testing seems to require looping, so let's try to merge loops
777
+
778
+ let clustered_column = true;
779
+ let histogram = true;
780
+ let box_whisker = true;
781
+
782
+ for (const series of ex_series) {
783
+
784
+ const layout = series.a$?.layoutId;
785
+
786
+ if (clustered_column && layout !== 'clusteredColumn') {
787
+ clustered_column = false;
788
+ }
789
+ if (box_whisker && layout !== 'boxWhisker') {
790
+ box_whisker = false;
791
+ }
792
+ if (clustered_column && histogram) {
793
+ const binning = XMLUtils.FindAll(series, `cx:layoutPr/cx:binning`);
794
+ if (!binning.length) {
795
+ histogram = false;
796
+ }
797
+ }
798
+
799
+ }
800
+
801
+ // ok that's what we know so far...
802
+
803
+ if (histogram) {
804
+ result.type = ChartType.Histogram;
805
+ result.series = [];
806
+
807
+ for (const series_entry of ex_series) {
808
+
809
+ if (series_entry.a$?.hidden === '1') {
810
+ continue;
811
+ }
812
+
813
+ const series: ChartSeries = {};
814
+
815
+ const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data');
816
+
817
+ // so there are multiple series, and multiple datasets,
818
+ // but they are all merged together? no idea how this design
819
+ // works
820
+
821
+ const values_list: string[] = [];
822
+ for (const data_series of data) {
823
+ values_list.push(data_series['cx:numDim']?.['cx:f'] || '');
824
+ }
825
+ series.values = values_list.join(',');
826
+
827
+ const bin_count = XMLUtils.FindAll(series_entry, `cx:layoutPr/cx:binning/cx:binCount`);
828
+ if (bin_count[0]) {
829
+ const count = Number(bin_count[0].a$?.val || 0);
830
+ if (count) {
831
+ series.bin_count = count;
832
+ }
833
+ }
834
+
835
+ result.series.push(series);
836
+
837
+ }
838
+
839
+ const title = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:title/cx:tx/cx:txData');
840
+ if (title) {
841
+ if (title[0]?.['cx:f']) {
842
+ result.title = title[0]['cx:f'];
843
+ }
844
+ else if (title[0]?.['cx:v']) {
845
+ result.title = '"' + title[0]['cx:v'] + '"';
846
+ }
847
+ }
848
+
849
+ console.info("histogram", result);
850
+ return result;
851
+
852
+ }
853
+
854
+ if (box_whisker) {
855
+
711
856
  result.type = ChartType.Box;
712
857
  result.series = [];
713
858
  const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data'); // /cx:data/cx:numDim/cx:f');
@@ -758,7 +903,8 @@ export class Workbook {
758
903
  }
759
904
 
760
905
  if (!node) {
761
- console.info("Chart type not handled");
906
+ console.info("Chart type not handled", {xml});
907
+ result.type = ChartType.Unknown;
762
908
  }
763
909
 
764
910
  // console.info("RX?", result);