documonster 0.18.1 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README_zh.md +1 -1
- package/dist/esm/modules/excel/xlsb/pivot-cache.js +7 -15
- package/dist/esm/modules/excel/xlsb/pivot-model.js +12 -1
- package/dist/esm/modules/excel/xlsb/pivot-view.js +58 -23
- package/dist/esm/modules/excel/xlsx/xform/pivot-table/cache-field-xform.js +2 -1
- package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-cache-records-xform.js +2 -1
- package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.js +214 -125
- package/dist/iife/documonster.archive.iife.min.js +1 -1
- package/dist/iife/documonster.csv.iife.min.js +1 -1
- package/dist/iife/documonster.draw.iife.min.js +1 -1
- package/dist/iife/documonster.excel.iife.min.js +53 -53
- package/dist/iife/documonster.formula.iife.min.js +1 -1
- package/dist/iife/documonster.markdown.iife.min.js +1 -1
- package/dist/iife/documonster.mermaid.iife.min.js +1 -1
- package/dist/iife/documonster.pdf.iife.min.js +1 -1
- package/dist/iife/documonster.stream.iife.min.js +1 -1
- package/dist/iife/documonster.word.iife.min.js +58 -58
- package/dist/iife/documonster.xml.iife.min.js +1 -1
- package/dist/types/modules/excel/core/pivot-table.d.ts +18 -0
- package/dist/types/modules/excel/xlsb/pivot-view.d.ts +16 -0
- package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-table-xform.d.ts +45 -3
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { VALID_SUBTOTALS, METRIC_DISPLAY_NAMES } from "../../../core/pivot-table-types.js";
|
|
2
2
|
import { colCache } from "../../../utils/col-cache.js";
|
|
3
3
|
import { BaseXform } from "../base-xform.js";
|
|
4
|
-
import { RawXmlCollector
|
|
4
|
+
import { RawXmlCollector } from "./raw-xml-collector.js";
|
|
5
|
+
import { parseXsdBoolean } from "../xsd-values.js";
|
|
5
6
|
import { StdDocAttributes } from "../../../../xml/writer.js";
|
|
6
7
|
/** OOXML sentinel field index meaning "data values" pseudo-field (used in pivotArea references) */
|
|
7
8
|
const FIELD_INDEX_DATA_VALUES = 4294967294; // 0xFFFFFFFE
|
|
@@ -15,15 +16,75 @@ const DEFAULT_PIVOT_STYLE = "PivotStyleLight16";
|
|
|
15
16
|
/** Valid OOXML axis values for pivot fields */
|
|
16
17
|
const VALID_PIVOT_AXES = new Set(["axisRow", "axisCol", "axisPage", "axisValues"]);
|
|
17
18
|
/** PivotFieldItem attribute keys — used to build the attrs object for rendering */
|
|
18
|
-
const PIVOT_FIELD_ITEM_KEYS = ["
|
|
19
|
+
const PIVOT_FIELD_ITEM_KEYS = ["n", "t", "h", "s", "sd", "f", "m", "c", "x", "d", "e"];
|
|
19
20
|
/** Factory for default ParserState values */
|
|
20
21
|
function createDefaultParserState() {
|
|
21
22
|
return {
|
|
22
|
-
currentSection: null
|
|
23
|
-
inPivotArea: false,
|
|
24
|
-
inAutoSortScope: false
|
|
23
|
+
currentSection: null
|
|
25
24
|
};
|
|
26
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Root `<pivotTableDefinition>` attributes that are parsed individually. Every
|
|
28
|
+
* other attribute is kept in `extraRootAttrs`: CT_pivotTableDefinition defines
|
|
29
|
+
* some seventy, and a whitelist that only re-emits the ones it names silently
|
|
30
|
+
* drops the rest (issue #238 lost rowHeaderCaption/colHeaderCaption that way).
|
|
31
|
+
*/
|
|
32
|
+
const KNOWN_ROOT_KEYS = new Set([
|
|
33
|
+
// xmlns:xr is deliberately absent: an original mc:Ignorable="xr" needs the
|
|
34
|
+
// prefix declared even when the table carries no xr:uid.
|
|
35
|
+
"xmlns",
|
|
36
|
+
"xr:uid",
|
|
37
|
+
"name",
|
|
38
|
+
"cacheId",
|
|
39
|
+
"applyNumberFormats",
|
|
40
|
+
"applyBorderFormats",
|
|
41
|
+
"applyFontFormats",
|
|
42
|
+
"applyPatternFormats",
|
|
43
|
+
"applyAlignmentFormats",
|
|
44
|
+
"applyWidthHeightFormats",
|
|
45
|
+
"dataCaption",
|
|
46
|
+
"updatedVersion",
|
|
47
|
+
"minRefreshableVersion",
|
|
48
|
+
"createdVersion",
|
|
49
|
+
"useAutoFormatting",
|
|
50
|
+
"itemPrintTitles",
|
|
51
|
+
"indent",
|
|
52
|
+
"compact",
|
|
53
|
+
"compactData",
|
|
54
|
+
"multipleFieldFilters",
|
|
55
|
+
"outline",
|
|
56
|
+
"outlineData",
|
|
57
|
+
"chartFormat",
|
|
58
|
+
"colGrandTotals",
|
|
59
|
+
"rowGrandTotals",
|
|
60
|
+
"showError",
|
|
61
|
+
"errorCaption",
|
|
62
|
+
"showMissing",
|
|
63
|
+
"missingCaption",
|
|
64
|
+
"grandTotalCaption",
|
|
65
|
+
"rowHeaderCaption",
|
|
66
|
+
"colHeaderCaption",
|
|
67
|
+
"tag"
|
|
68
|
+
]);
|
|
69
|
+
/** dataField attributes that are parsed individually */
|
|
70
|
+
const KNOWN_DATA_FIELD_KEYS = new Set([
|
|
71
|
+
"name",
|
|
72
|
+
"fld",
|
|
73
|
+
"subtotal",
|
|
74
|
+
"baseField",
|
|
75
|
+
"baseItem",
|
|
76
|
+
"numFmtId"
|
|
77
|
+
]);
|
|
78
|
+
/** Collect the attributes not named in `known`, or undefined when there are none. */
|
|
79
|
+
function collectExtraAttrs(attributes, known) {
|
|
80
|
+
let extra;
|
|
81
|
+
for (const [k, v] of Object.entries(attributes)) {
|
|
82
|
+
if (!known.has(k) && v != null) {
|
|
83
|
+
(extra ?? (extra = {}))[k] = String(v);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return extra;
|
|
87
|
+
}
|
|
27
88
|
/** Known pivotField attributes that we parse individually (hoisted to module scope) */
|
|
28
89
|
const KNOWN_PIVOT_FIELD_KEYS = new Set([
|
|
29
90
|
"axis",
|
|
@@ -48,12 +109,17 @@ class PivotTableXform extends BaseXform {
|
|
|
48
109
|
this.currentRowItem = null;
|
|
49
110
|
this.currentColItem = null;
|
|
50
111
|
this.currentChartFormat = null;
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
// Buffer for collecting autoSortScope XML
|
|
54
|
-
this.autoSortScopeXmlBuffer = [];
|
|
112
|
+
this.currentDataField = null;
|
|
113
|
+
this.currentPageField = null;
|
|
55
114
|
// Raw XML collectors (replacing manual in/depth/buffer triples)
|
|
56
115
|
this.extLstCollector = new RawXmlCollector("extLst");
|
|
116
|
+
/**
|
|
117
|
+
* Verbatim capture of a subtree that belongs to the element being parsed —
|
|
118
|
+
* a field's own `<extLst>`, a pivotField's `<autoSortScope>`, a chartFormat's
|
|
119
|
+
* `<pivotArea>`. `commit` receives the XML once the subtree closes.
|
|
120
|
+
*/
|
|
121
|
+
this.nestedCollector = new RawXmlCollector("");
|
|
122
|
+
this.nestedCommit = null;
|
|
57
123
|
this.formatsCollector = new RawXmlCollector("formats");
|
|
58
124
|
this.conditionalFormatsCollector = new RawXmlCollector("conditionalFormats");
|
|
59
125
|
this.filtersCollector = new RawXmlCollector("filters");
|
|
@@ -75,8 +141,10 @@ class PivotTableXform extends BaseXform {
|
|
|
75
141
|
this.currentRowItem = null;
|
|
76
142
|
this.currentColItem = null;
|
|
77
143
|
this.currentChartFormat = null;
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
144
|
+
this.currentDataField = null;
|
|
145
|
+
this.currentPageField = null;
|
|
146
|
+
this.nestedCollector.reset();
|
|
147
|
+
this.nestedCommit = null;
|
|
80
148
|
this.extLstCollector.reset();
|
|
81
149
|
this.formatsCollector.reset();
|
|
82
150
|
this.conditionalFormatsCollector.reset();
|
|
@@ -355,7 +423,17 @@ class PivotTableXform extends BaseXform {
|
|
|
355
423
|
if (pf.name !== undefined) {
|
|
356
424
|
pfAttrs.name = pf.name;
|
|
357
425
|
}
|
|
358
|
-
|
|
426
|
+
if (pf.cap !== undefined) {
|
|
427
|
+
pfAttrs.cap = pf.cap;
|
|
428
|
+
}
|
|
429
|
+
if (pf.extLstXml) {
|
|
430
|
+
xmlStream.openNode("pageField", pfAttrs);
|
|
431
|
+
xmlStream.writeRaw(pf.extLstXml);
|
|
432
|
+
xmlStream.closeNode();
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
xmlStream.leafNode("pageField", pfAttrs);
|
|
436
|
+
}
|
|
359
437
|
}
|
|
360
438
|
xmlStream.closeNode();
|
|
361
439
|
}
|
|
@@ -379,7 +457,19 @@ class PivotTableXform extends BaseXform {
|
|
|
379
457
|
if (dataField.numFmtId !== undefined) {
|
|
380
458
|
dfAttrs.numFmtId = dataField.numFmtId;
|
|
381
459
|
}
|
|
382
|
-
|
|
460
|
+
if (dataField.extraAttrs) {
|
|
461
|
+
for (const [k, v] of Object.entries(dataField.extraAttrs)) {
|
|
462
|
+
dfAttrs[k] ?? (dfAttrs[k] = v);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (dataField.extLstXml) {
|
|
466
|
+
xmlStream.openNode("dataField", dfAttrs);
|
|
467
|
+
xmlStream.writeRaw(dataField.extLstXml);
|
|
468
|
+
xmlStream.closeNode();
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
xmlStream.leafNode("dataField", dfAttrs);
|
|
472
|
+
}
|
|
383
473
|
}
|
|
384
474
|
xmlStream.closeNode();
|
|
385
475
|
}
|
|
@@ -494,6 +584,15 @@ class PivotTableXform extends BaseXform {
|
|
|
494
584
|
if (model.grandTotalCaption !== undefined) {
|
|
495
585
|
attrs.grandTotalCaption = model.grandTotalCaption;
|
|
496
586
|
}
|
|
587
|
+
if (model.rowHeaderCaption !== undefined) {
|
|
588
|
+
attrs.rowHeaderCaption = model.rowHeaderCaption;
|
|
589
|
+
}
|
|
590
|
+
if (model.colHeaderCaption !== undefined) {
|
|
591
|
+
attrs.colHeaderCaption = model.colHeaderCaption;
|
|
592
|
+
}
|
|
593
|
+
if (model.tag !== undefined) {
|
|
594
|
+
attrs.tag = model.tag;
|
|
595
|
+
}
|
|
497
596
|
// Only write compact/compactData when false (non-default).
|
|
498
597
|
// OOXML spec: absent = true (default). So if the original file had compact="0",
|
|
499
598
|
// we must preserve it; omitting it would change semantics from false to true.
|
|
@@ -503,6 +602,11 @@ class PivotTableXform extends BaseXform {
|
|
|
503
602
|
if (model.compactData === false) {
|
|
504
603
|
attrs.compactData = "0";
|
|
505
604
|
}
|
|
605
|
+
if (model.extraRootAttrs) {
|
|
606
|
+
for (const [k, v] of Object.entries(model.extraRootAttrs)) {
|
|
607
|
+
attrs[k] ?? (attrs[k] = v);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
506
610
|
return attrs;
|
|
507
611
|
}
|
|
508
612
|
/**
|
|
@@ -605,7 +709,9 @@ class PivotTableXform extends BaseXform {
|
|
|
605
709
|
attrs[k] = v;
|
|
606
710
|
}
|
|
607
711
|
}
|
|
608
|
-
const hasChildren = (field.items !== undefined && field.items.length > 0) ||
|
|
712
|
+
const hasChildren = (field.items !== undefined && field.items.length > 0) ||
|
|
713
|
+
field.autoSortScopeXml !== undefined ||
|
|
714
|
+
field.extLstXml !== undefined;
|
|
609
715
|
if (hasChildren) {
|
|
610
716
|
xmlStream.openNode("pivotField", attrs);
|
|
611
717
|
if (field.items !== undefined && field.items.length > 0) {
|
|
@@ -624,6 +730,9 @@ class PivotTableXform extends BaseXform {
|
|
|
624
730
|
if (field.autoSortScopeXml) {
|
|
625
731
|
xmlStream.writeRaw(field.autoSortScopeXml);
|
|
626
732
|
}
|
|
733
|
+
if (field.extLstXml) {
|
|
734
|
+
xmlStream.writeRaw(field.extLstXml);
|
|
735
|
+
}
|
|
627
736
|
xmlStream.closeNode(); // pivotField
|
|
628
737
|
}
|
|
629
738
|
else {
|
|
@@ -635,7 +744,18 @@ class PivotTableXform extends BaseXform {
|
|
|
635
744
|
// approach, while verbose, handles all OOXML edge cases correctly.
|
|
636
745
|
parseOpen(node) {
|
|
637
746
|
const { name, attributes } = node;
|
|
638
|
-
//
|
|
747
|
+
// A new root always starts a fresh table, even if a previous parse on this
|
|
748
|
+
// instance was abandoned mid-capture. The unprefixed main-namespace root
|
|
749
|
+
// cannot legally occur inside a captured subtree (extension content is
|
|
750
|
+
// namespaced, e.g. x14:pivotTableDefinition), so this cannot cut one short.
|
|
751
|
+
if (name === this.tag) {
|
|
752
|
+
this.reset();
|
|
753
|
+
}
|
|
754
|
+
// Collect raw XML verbatim for roundtrip preservation
|
|
755
|
+
if (this.nestedCollector.active) {
|
|
756
|
+
this.nestedCollector.feedOpen(name, attributes);
|
|
757
|
+
return true;
|
|
758
|
+
}
|
|
639
759
|
if (this.extLstCollector.active) {
|
|
640
760
|
this.extLstCollector.feedOpen(name, attributes);
|
|
641
761
|
return true;
|
|
@@ -658,8 +778,7 @@ class PivotTableXform extends BaseXform {
|
|
|
658
778
|
}
|
|
659
779
|
switch (name) {
|
|
660
780
|
case this.tag:
|
|
661
|
-
// pivotTableDefinition root element
|
|
662
|
-
this.reset();
|
|
781
|
+
// pivotTableDefinition root element (state was reset above)
|
|
663
782
|
this.model = {
|
|
664
783
|
name: attributes.name,
|
|
665
784
|
cacheId: parseInt(attributes.cacheId ?? "0", 10),
|
|
@@ -682,11 +801,11 @@ class PivotTableXform extends BaseXform {
|
|
|
682
801
|
useAutoFormatting: attributes.useAutoFormatting,
|
|
683
802
|
itemPrintTitles: attributes.itemPrintTitles,
|
|
684
803
|
indent: attributes.indent !== undefined ? parseInt(attributes.indent, 10) : undefined,
|
|
685
|
-
compact: attributes.compact
|
|
686
|
-
compactData: attributes.compactData
|
|
804
|
+
compact: parseXsdBoolean(attributes.compact) ?? true,
|
|
805
|
+
compactData: parseXsdBoolean(attributes.compactData) ?? true,
|
|
687
806
|
multipleFieldFilters: attributes.multipleFieldFilters,
|
|
688
|
-
outline: attributes.outline
|
|
689
|
-
outlineData: attributes.outlineData
|
|
807
|
+
outline: parseXsdBoolean(attributes.outline) ?? false,
|
|
808
|
+
outlineData: parseXsdBoolean(attributes.outlineData) ?? false,
|
|
690
809
|
chartFormat: attributes.chartFormat !== undefined ? parseInt(attributes.chartFormat, 10) : undefined,
|
|
691
810
|
colGrandTotals: attributes.colGrandTotals,
|
|
692
811
|
rowGrandTotals: attributes.rowGrandTotals,
|
|
@@ -695,6 +814,10 @@ class PivotTableXform extends BaseXform {
|
|
|
695
814
|
showMissing: attributes.showMissing,
|
|
696
815
|
missingCaption: attributes.missingCaption,
|
|
697
816
|
grandTotalCaption: attributes.grandTotalCaption,
|
|
817
|
+
rowHeaderCaption: attributes.rowHeaderCaption,
|
|
818
|
+
colHeaderCaption: attributes.colHeaderCaption,
|
|
819
|
+
tag: attributes.tag,
|
|
820
|
+
extraRootAttrs: collectExtraAttrs(attributes, KNOWN_ROOT_KEYS),
|
|
698
821
|
rowItems: [],
|
|
699
822
|
colItems: [],
|
|
700
823
|
chartFormats: [],
|
|
@@ -728,29 +851,23 @@ class PivotTableXform extends BaseXform {
|
|
|
728
851
|
break;
|
|
729
852
|
case "pivotField":
|
|
730
853
|
if (this.state.currentSection === "pivotFields") {
|
|
731
|
-
// Collect unknown attributes into extraAttrs bag for roundtrip preservation
|
|
732
|
-
const extraAttrs = {};
|
|
733
|
-
for (const [k, v] of Object.entries(attributes)) {
|
|
734
|
-
if (!KNOWN_PIVOT_FIELD_KEYS.has(k)) {
|
|
735
|
-
extraAttrs[k] = String(v);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
854
|
this.currentPivotField = {
|
|
739
855
|
axis: VALID_PIVOT_AXES.has(attributes.axis)
|
|
740
856
|
? attributes.axis
|
|
741
857
|
: undefined,
|
|
742
|
-
dataField: attributes.dataField
|
|
858
|
+
dataField: parseXsdBoolean(attributes.dataField) ?? false,
|
|
743
859
|
items: [],
|
|
744
|
-
compact: attributes.compact
|
|
745
|
-
outline: attributes.outline
|
|
746
|
-
showAll: attributes.showAll
|
|
747
|
-
defaultSubtotal: attributes.defaultSubtotal
|
|
860
|
+
compact: parseXsdBoolean(attributes.compact) ?? true,
|
|
861
|
+
outline: parseXsdBoolean(attributes.outline) ?? true,
|
|
862
|
+
showAll: parseXsdBoolean(attributes.showAll) ?? true,
|
|
863
|
+
defaultSubtotal: parseXsdBoolean(attributes.defaultSubtotal) ?? true,
|
|
748
864
|
numFmtId: attributes.numFmtId !== undefined ? parseInt(attributes.numFmtId, 10) : undefined,
|
|
749
865
|
sortType: attributes.sortType,
|
|
750
|
-
subtotalTop: attributes.subtotalTop
|
|
751
|
-
insertBlankRow: attributes.insertBlankRow ===
|
|
752
|
-
multipleItemSelectionAllowed: attributes.multipleItemSelectionAllowed ===
|
|
753
|
-
|
|
866
|
+
subtotalTop: parseXsdBoolean(attributes.subtotalTop),
|
|
867
|
+
insertBlankRow: parseXsdBoolean(attributes.insertBlankRow) === true ? true : undefined,
|
|
868
|
+
multipleItemSelectionAllowed: parseXsdBoolean(attributes.multipleItemSelectionAllowed) === true ? true : undefined,
|
|
869
|
+
// Unknown attributes kept verbatim for roundtrip preservation
|
|
870
|
+
extraAttrs: collectExtraAttrs(attributes, KNOWN_PIVOT_FIELD_KEYS)
|
|
754
871
|
};
|
|
755
872
|
}
|
|
756
873
|
break;
|
|
@@ -776,8 +893,10 @@ class PivotTableXform extends BaseXform {
|
|
|
776
893
|
case "autoSortScope":
|
|
777
894
|
// Start collecting autoSortScope XML for the current pivotField
|
|
778
895
|
if (this.currentPivotField) {
|
|
779
|
-
|
|
780
|
-
this.
|
|
896
|
+
const field = this.currentPivotField;
|
|
897
|
+
this.startNested(name, attributes, xml => {
|
|
898
|
+
field.autoSortScopeXml = xml;
|
|
899
|
+
});
|
|
781
900
|
}
|
|
782
901
|
break;
|
|
783
902
|
case "rowFields":
|
|
@@ -798,12 +917,14 @@ class PivotTableXform extends BaseXform {
|
|
|
798
917
|
break;
|
|
799
918
|
case "pageField":
|
|
800
919
|
if (this.state.currentSection === "pageFields" && this.model) {
|
|
801
|
-
this.
|
|
920
|
+
this.currentPageField = {
|
|
802
921
|
fld: parseInt(attributes.fld ?? "0", 10),
|
|
803
922
|
item: attributes.item !== undefined ? parseInt(attributes.item, 10) : undefined,
|
|
804
923
|
hier: attributes.hier !== undefined ? parseInt(attributes.hier, 10) : undefined,
|
|
805
|
-
name: attributes.name
|
|
806
|
-
|
|
924
|
+
name: attributes.name,
|
|
925
|
+
cap: attributes.cap
|
|
926
|
+
};
|
|
927
|
+
this.model.pageFields.push(this.currentPageField);
|
|
807
928
|
}
|
|
808
929
|
break;
|
|
809
930
|
case "rowItems":
|
|
@@ -833,18 +954,8 @@ class PivotTableXform extends BaseXform {
|
|
|
833
954
|
}
|
|
834
955
|
break;
|
|
835
956
|
case "x":
|
|
836
|
-
//
|
|
837
|
-
if (this.
|
|
838
|
-
// Collect x element for pivotArea XML (re-encode attribute values for XML safety)
|
|
839
|
-
const xAttrs = serializeAttributes(attributes);
|
|
840
|
-
if (this.state.inAutoSortScope) {
|
|
841
|
-
this.autoSortScopeXmlBuffer.push(xAttrs ? `<x ${xAttrs}/>` : "<x/>");
|
|
842
|
-
}
|
|
843
|
-
else {
|
|
844
|
-
this.pivotAreaXmlBuffer.push(xAttrs ? `<x ${xAttrs}/>` : "<x/>");
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
else if (this.currentRowItem) {
|
|
957
|
+
// x element inside row/col items (pivotArea x elements are captured verbatim)
|
|
958
|
+
if (this.currentRowItem) {
|
|
848
959
|
this.currentRowItem.x.push({ v: parseInt(attributes.v ?? "0", 10) });
|
|
849
960
|
}
|
|
850
961
|
else if (this.currentColItem) {
|
|
@@ -859,34 +970,17 @@ class PivotTableXform extends BaseXform {
|
|
|
859
970
|
this.currentChartFormat = {
|
|
860
971
|
chart: parseInt(attributes.chart ?? "0", 10),
|
|
861
972
|
format: parseInt(attributes.format ?? "0", 10),
|
|
862
|
-
series: attributes.series
|
|
973
|
+
series: parseXsdBoolean(attributes.series)
|
|
863
974
|
};
|
|
864
975
|
}
|
|
865
976
|
break;
|
|
866
977
|
case "pivotArea":
|
|
867
|
-
//
|
|
978
|
+
// A chartFormat's pivotArea is kept verbatim for roundtrip
|
|
868
979
|
if (this.currentChartFormat) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
else if (this.state.inAutoSortScope) {
|
|
874
|
-
this.state.inPivotArea = true;
|
|
875
|
-
const attrsStr = serializeAttributes(attributes);
|
|
876
|
-
this.autoSortScopeXmlBuffer.push(attrsStr ? `<pivotArea ${attrsStr}>` : "<pivotArea>");
|
|
877
|
-
}
|
|
878
|
-
break;
|
|
879
|
-
case "references":
|
|
880
|
-
case "reference":
|
|
881
|
-
// Collect nested elements in pivotArea
|
|
882
|
-
if (this.state.inPivotArea) {
|
|
883
|
-
const attrsStr = serializeAttributes(attributes);
|
|
884
|
-
if (this.state.inAutoSortScope) {
|
|
885
|
-
this.autoSortScopeXmlBuffer.push(`<${name}${attrsStr ? " " + attrsStr : ""}>`);
|
|
886
|
-
}
|
|
887
|
-
else {
|
|
888
|
-
this.pivotAreaXmlBuffer.push(`<${name}${attrsStr ? " " + attrsStr : ""}>`);
|
|
889
|
-
}
|
|
980
|
+
const chartFormat = this.currentChartFormat;
|
|
981
|
+
this.startNested(name, attributes, xml => {
|
|
982
|
+
chartFormat.pivotAreaXml = xml;
|
|
983
|
+
});
|
|
890
984
|
}
|
|
891
985
|
break;
|
|
892
986
|
case "field":
|
|
@@ -903,7 +997,7 @@ class PivotTableXform extends BaseXform {
|
|
|
903
997
|
break;
|
|
904
998
|
case "dataField":
|
|
905
999
|
if (this.state.currentSection === "dataFields" && this.model) {
|
|
906
|
-
this.
|
|
1000
|
+
this.currentDataField = {
|
|
907
1001
|
name: attributes.name ?? "",
|
|
908
1002
|
fld: parseInt(attributes.fld ?? "0", 10),
|
|
909
1003
|
baseField: attributes.baseField !== undefined ? parseInt(attributes.baseField, 10) : undefined,
|
|
@@ -911,8 +1005,10 @@ class PivotTableXform extends BaseXform {
|
|
|
911
1005
|
subtotal: VALID_SUBTOTALS.has(attributes.subtotal)
|
|
912
1006
|
? attributes.subtotal
|
|
913
1007
|
: undefined,
|
|
914
|
-
numFmtId: attributes.numFmtId !== undefined ? parseInt(attributes.numFmtId, 10) : undefined
|
|
915
|
-
|
|
1008
|
+
numFmtId: attributes.numFmtId !== undefined ? parseInt(attributes.numFmtId, 10) : undefined,
|
|
1009
|
+
extraAttrs: collectExtraAttrs(attributes, KNOWN_DATA_FIELD_KEYS)
|
|
1010
|
+
};
|
|
1011
|
+
this.model.dataFields.push(this.currentDataField);
|
|
916
1012
|
}
|
|
917
1013
|
break;
|
|
918
1014
|
case "pivotTableStyleInfo":
|
|
@@ -929,9 +1025,20 @@ class PivotTableXform extends BaseXform {
|
|
|
929
1025
|
}
|
|
930
1026
|
break;
|
|
931
1027
|
case "extLst":
|
|
932
|
-
// Start collecting extLst XML for roundtrip preservation
|
|
1028
|
+
// Start collecting extLst XML for roundtrip preservation. A pivotField or
|
|
1029
|
+
// dataField may carry its own extLst; it belongs to that field, not the table.
|
|
933
1030
|
if (this.model) {
|
|
934
|
-
|
|
1031
|
+
// Per CT_PivotField / CT_DataField / CT_PageField these are the only
|
|
1032
|
+
// modeled elements that carry an extLst besides the table itself.
|
|
1033
|
+
const owner = this.currentPivotField ?? this.currentDataField ?? this.currentPageField;
|
|
1034
|
+
if (owner) {
|
|
1035
|
+
this.startNested(name, attributes, xml => {
|
|
1036
|
+
owner.extLstXml = xml;
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
this.extLstCollector.start(attributes);
|
|
1041
|
+
}
|
|
935
1042
|
}
|
|
936
1043
|
break;
|
|
937
1044
|
case "formats":
|
|
@@ -958,12 +1065,9 @@ class PivotTableXform extends BaseXform {
|
|
|
958
1065
|
// This preserves elements like pivotHierarchies, rowHierarchiesUsage,
|
|
959
1066
|
// colHierarchiesUsage, etc. that we don't individually model.
|
|
960
1067
|
// R8-B1: Only activate at the top level of pivotTableDefinition — NOT inside
|
|
961
|
-
// known sections (pivotFields, rowFields, etc.)
|
|
1068
|
+
// known sections (pivotFields, rowFields, etc.),
|
|
962
1069
|
// otherwise the collector would steal subsequent tags from normal parsing.
|
|
963
|
-
if (this.model &&
|
|
964
|
-
this.state.currentSection === null &&
|
|
965
|
-
!this.state.inPivotArea &&
|
|
966
|
-
!this.state.inAutoSortScope) {
|
|
1070
|
+
if (this.model && this.state.currentSection === null) {
|
|
967
1071
|
this.unknownCollector.startAs(name, attributes);
|
|
968
1072
|
}
|
|
969
1073
|
break;
|
|
@@ -972,7 +1076,10 @@ class PivotTableXform extends BaseXform {
|
|
|
972
1076
|
}
|
|
973
1077
|
parseText(text) {
|
|
974
1078
|
// Forward text nodes to whichever raw-XML collector is active (B3 fix)
|
|
975
|
-
if (this.
|
|
1079
|
+
if (this.nestedCollector.active) {
|
|
1080
|
+
this.nestedCollector.feedText(text);
|
|
1081
|
+
}
|
|
1082
|
+
else if (this.extLstCollector.active) {
|
|
976
1083
|
this.extLstCollector.feedText(text);
|
|
977
1084
|
}
|
|
978
1085
|
else if (this.formatsCollector.active) {
|
|
@@ -988,6 +1095,11 @@ class PivotTableXform extends BaseXform {
|
|
|
988
1095
|
this.unknownCollector.feedText(text);
|
|
989
1096
|
}
|
|
990
1097
|
}
|
|
1098
|
+
/** Begin capturing the subtree rooted at `name`; `commit` receives it when it closes. */
|
|
1099
|
+
startNested(name, attributes, commit) {
|
|
1100
|
+
this.nestedCommit = commit;
|
|
1101
|
+
this.nestedCollector.startAs(name, attributes);
|
|
1102
|
+
}
|
|
991
1103
|
/** Feed a close-tag to a collector; if it completes, store the result on the model. */
|
|
992
1104
|
tryCloseCollector(collector, name, modelKey) {
|
|
993
1105
|
if (collector.feedClose(name)) {
|
|
@@ -999,6 +1111,14 @@ class PivotTableXform extends BaseXform {
|
|
|
999
1111
|
}
|
|
1000
1112
|
parseClose(name) {
|
|
1001
1113
|
// Handle raw-XML collectors — close tags
|
|
1114
|
+
if (this.nestedCollector.active) {
|
|
1115
|
+
if (this.nestedCollector.feedClose(name)) {
|
|
1116
|
+
this.nestedCommit?.(this.nestedCollector.result);
|
|
1117
|
+
this.nestedCommit = null;
|
|
1118
|
+
this.nestedCollector.reset();
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1002
1122
|
if (this.extLstCollector.active) {
|
|
1003
1123
|
this.tryCloseCollector(this.extLstCollector, name, "extLstXml");
|
|
1004
1124
|
return true;
|
|
@@ -1022,34 +1142,6 @@ class PivotTableXform extends BaseXform {
|
|
|
1022
1142
|
}
|
|
1023
1143
|
return true;
|
|
1024
1144
|
}
|
|
1025
|
-
// Handle pivotArea nested elements - close tags
|
|
1026
|
-
if (this.state.inPivotArea) {
|
|
1027
|
-
if (name === "pivotArea") {
|
|
1028
|
-
if (this.state.inAutoSortScope) {
|
|
1029
|
-
this.autoSortScopeXmlBuffer.push("</pivotArea>");
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
this.pivotAreaXmlBuffer.push("</pivotArea>");
|
|
1033
|
-
if (this.currentChartFormat) {
|
|
1034
|
-
this.currentChartFormat.pivotAreaXml = this.pivotAreaXmlBuffer.join("");
|
|
1035
|
-
}
|
|
1036
|
-
this.pivotAreaXmlBuffer = [];
|
|
1037
|
-
}
|
|
1038
|
-
this.state.inPivotArea = false;
|
|
1039
|
-
return true;
|
|
1040
|
-
}
|
|
1041
|
-
else if (name === "references" || name === "reference") {
|
|
1042
|
-
if (this.state.inAutoSortScope) {
|
|
1043
|
-
this.autoSortScopeXmlBuffer.push(`</${name}>`);
|
|
1044
|
-
}
|
|
1045
|
-
else {
|
|
1046
|
-
this.pivotAreaXmlBuffer.push(`</${name}>`);
|
|
1047
|
-
}
|
|
1048
|
-
return true;
|
|
1049
|
-
}
|
|
1050
|
-
// x elements are self-closing, no need to handle close
|
|
1051
|
-
return true;
|
|
1052
|
-
}
|
|
1053
1145
|
switch (name) {
|
|
1054
1146
|
case this.tag:
|
|
1055
1147
|
// End of pivotTableDefinition — store any collected unknown elements
|
|
@@ -1073,18 +1165,15 @@ class PivotTableXform extends BaseXform {
|
|
|
1073
1165
|
this.currentPivotField = null;
|
|
1074
1166
|
}
|
|
1075
1167
|
break;
|
|
1168
|
+
case "dataField":
|
|
1169
|
+
this.currentDataField = null;
|
|
1170
|
+
break;
|
|
1171
|
+
case "pageField":
|
|
1172
|
+
this.currentPageField = null;
|
|
1173
|
+
break;
|
|
1076
1174
|
case "items":
|
|
1077
1175
|
// No close handling needed — item parsing guarded by currentPivotField
|
|
1078
1176
|
break;
|
|
1079
|
-
case "autoSortScope":
|
|
1080
|
-
// Finish collecting autoSortScope XML
|
|
1081
|
-
if (this.state.inAutoSortScope && this.currentPivotField) {
|
|
1082
|
-
this.autoSortScopeXmlBuffer.push("</autoSortScope>");
|
|
1083
|
-
this.currentPivotField.autoSortScopeXml = this.autoSortScopeXmlBuffer.join("");
|
|
1084
|
-
this.autoSortScopeXmlBuffer = [];
|
|
1085
|
-
this.state.inAutoSortScope = false;
|
|
1086
|
-
}
|
|
1087
|
-
break;
|
|
1088
1177
|
case "i":
|
|
1089
1178
|
// Finish row/col item
|
|
1090
1179
|
if (this.currentRowItem && this.model) {
|