@openpresentation/opf-pptx 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -37
- package/dist/annotation-export.js +1 -1
- package/dist/annotation-provenance.js +4 -0
- package/dist/background-import.js +33 -17
- package/dist/background.js +30 -28
- package/dist/body-text-import.js +16 -4
- package/dist/chart-alt.js +54 -0
- package/dist/chart-data.js +29 -54
- package/dist/chart-highlight.js +113 -0
- package/dist/chart-options.js +46 -24
- package/dist/chart-types.js +61 -75
- package/dist/code-provenance.js +20 -4
- package/dist/color-ref.js +12 -6
- package/dist/data-provenance.js +18 -14
- package/dist/document-provenance.js +208 -129
- package/dist/format-check.js +10 -0
- package/dist/furniture-provenance.js +30 -3
- package/dist/heading-provenance.js +10 -3
- package/dist/image-fallback-node.js +4 -1
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +57 -35
- package/dist/index.js +724 -399
- package/dist/media-provenance.js +3 -3
- package/dist/metric-provenance.js +3 -3
- package/dist/native-text-style.js +2 -0
- package/dist/package-fonts.js +2 -2
- package/dist/quote-provenance.js +70 -26
- package/dist/rich-heading.js +119 -0
- package/dist/run-colors.js +3 -0
- package/dist/script-fonts.js +102 -101
- package/dist/theme-colors.js +16 -11
- package/dist/timeline-provenance.js +17 -6
- package/dist/typeface-inventory.js +8 -8
- package/dist/watermark-provenance.js +65 -3
- package/package.json +4 -4
- package/dist/slide-image-provenance.js +0 -177
package/dist/chart-types.js
CHANGED
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
// OPF chart type ids -> native PowerPoint chart constructs (FF-22).
|
|
2
2
|
//
|
|
3
3
|
// The core catalog keeps one chart type per Aspose.Slides ChartType
|
|
4
|
-
// (spec/catalogs/chart-types, OpenPresentation/opf#121)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// map to the exact Office construct; deprecated ids resolve to their
|
|
8
|
-
// replacement; any other id keeps the legacy substring heuristic.
|
|
4
|
+
// (spec/catalogs/chart-types, OpenPresentation/opf#121). The mapping is carried
|
|
5
|
+
// here so the exporter stays self-contained. Every id maps to the exact Office construct (OPF 0.15: chart.type is a
|
|
6
|
+
// schema enum, so there is no other id).
|
|
9
7
|
|
|
10
8
|
const category = (pptx, extra = {}) => ({ family: 'category', pptx, ...extra });
|
|
11
9
|
|
|
12
10
|
export const CHART_TYPES = Object.freeze({
|
|
13
11
|
column: category('bar', { aspose: 'ClusteredColumn', barDir: 'col', grouping: 'clustered' }),
|
|
14
|
-
'stacked-column
|
|
15
|
-
'100pct-stacked-column
|
|
12
|
+
'stacked-column': category('bar', { aspose: 'StackedColumn', barDir: 'col', grouping: 'stacked' }),
|
|
13
|
+
'100pct-stacked-column': category('bar', { aspose: 'PercentsStackedColumn', barDir: 'col', grouping: 'percentStacked' }),
|
|
16
14
|
bar: category('bar', { aspose: 'ClusteredBar', barDir: 'bar', grouping: 'clustered' }),
|
|
17
|
-
'stacked-bar
|
|
18
|
-
'100pct-stacked-bar
|
|
15
|
+
'stacked-bar': category('bar', { aspose: 'StackedBar', barDir: 'bar', grouping: 'stacked' }),
|
|
16
|
+
'100pct-stacked-bar': category('bar', { aspose: 'PercentsStackedBar', barDir: 'bar', grouping: 'percentStacked' }),
|
|
19
17
|
line: category('line', { aspose: 'Line', grouping: 'standard', markers: false }),
|
|
20
18
|
'line-with-markers': category('line', { aspose: 'LineWithMarkers', grouping: 'standard', markers: true }),
|
|
21
|
-
'stacked-line
|
|
22
|
-
'stacked-line-with-markers
|
|
19
|
+
'stacked-line': category('line', { aspose: 'StackedLine', grouping: 'stacked', markers: false }),
|
|
20
|
+
'stacked-line-with-markers': category('line', { aspose: 'StackedLineWithMarkers', grouping: 'stacked', markers: true }),
|
|
23
21
|
area: category('area', { aspose: 'Area', grouping: 'standard' }),
|
|
24
|
-
'stacked-area
|
|
25
|
-
'100pct-stacked-area
|
|
22
|
+
'stacked-area': category('area', { aspose: 'StackedArea', grouping: 'stacked' }),
|
|
23
|
+
'100pct-stacked-area': category('area', { aspose: 'PercentsStackedArea', grouping: 'percentStacked' }),
|
|
26
24
|
pie: { family: 'circular', pptx: 'pie', aspose: 'Pie' },
|
|
27
25
|
doughnut: { family: 'circular', pptx: 'doughnut', aspose: 'Doughnut' },
|
|
28
26
|
scatter: { family: 'xy', pptx: 'scatter', aspose: 'ScatterWithMarkers' },
|
|
29
27
|
radar: category('radar', { aspose: 'Radar', radarStyle: 'standard', markers: false }),
|
|
30
28
|
'radar-with-markers': category('radar', { aspose: 'RadarWithMarkers', radarStyle: 'marker', markers: true }),
|
|
31
29
|
'filled-radar': category('radar', { aspose: 'FilledRadar', radarStyle: 'filled', markers: false }),
|
|
30
|
+
// FA-15: clustered columns (a c:barChart on the primary axes) with line series with markers (a c:lineChart per value axis;
|
|
31
|
+
// the secondary one crosses at the right on a deleted second c:catAx). Core's resolveChartData says which series is which.
|
|
32
|
+
combo: { family: 'combo', pptx: 'bar', aspose: 'ClusteredColumn', barDir: 'col', grouping: 'clustered', markers: true },
|
|
32
33
|
// Office 2016 chartex constructs (cx:chartSpace, one cx:series per plot,
|
|
33
34
|
// named by layoutId). `requires` is the markup-compatibility namespace the
|
|
34
35
|
// slide's mc:Choice names, so older readers take the classic fallback.
|
|
@@ -75,65 +76,12 @@ export function chartTypeFromChartex(layoutIds) {
|
|
|
75
76
|
return null;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
export const DEPRECATED_CHART_TYPES = Object.freeze({
|
|
82
|
-
...variants('100pct-bullet-bar', '100pct-stacked-bar-3x'),
|
|
83
|
-
'100pct-progress-bar': '100pct-stacked-bar-3x',
|
|
84
|
-
'100pct-stacked-bar-2x': '100pct-stacked-bar-3x',
|
|
85
|
-
...variants('100pct-bullet-column', '100pct-stacked-column-3x'),
|
|
86
|
-
'100pct-stacked-column-2x': '100pct-stacked-column-3x',
|
|
87
|
-
'100pct-stacked-area-2x': '100pct-stacked-area-3x',
|
|
88
|
-
australia: 'world',
|
|
89
|
-
canada: 'world',
|
|
90
|
-
'united-kingdom': 'world',
|
|
91
|
-
'united-states': 'world',
|
|
92
|
-
'box-and-whisker-2x': 'box-and-whisker',
|
|
93
|
-
'box-and-whisker-3x': 'box-and-whisker',
|
|
94
|
-
...variants('bullet-bar', 'bar'),
|
|
95
|
-
'clustered-bar-2x': 'bar',
|
|
96
|
-
...variants('bullet-column', 'column'),
|
|
97
|
-
'clustered-column': 'column',
|
|
98
|
-
...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`dot-plot${suffix}`, 'scatter'])),
|
|
99
|
-
dumbbell: 'scatter',
|
|
100
|
-
'line-2x': 'line',
|
|
101
|
-
'line-3x': 'line',
|
|
102
|
-
'line-with-high-low': 'line',
|
|
103
|
-
...Object.fromEntries(['', '-2x', '-3x', '-4x', '-5x', '-6x'].map((suffix) => [`sparkline${suffix}`, 'line'])),
|
|
104
|
-
'line-with-high-low-and-markers': 'line-with-markers',
|
|
105
|
-
'line-with-markers-2x': 'line-with-markers',
|
|
106
|
-
'line-with-markers-3x': 'line-with-markers',
|
|
107
|
-
'stacked-area-2x': 'stacked-area-3x',
|
|
108
|
-
'stacked-bar-2x': 'stacked-bar-3x',
|
|
109
|
-
'stacked-column-2x': 'stacked-column-3x',
|
|
110
|
-
'stacked-line-2x': 'stacked-line-3x',
|
|
111
|
-
'stacked-line-with-markers-2x': 'stacked-line-with-markers-3x',
|
|
112
|
-
'treemap-2x': 'treemap',
|
|
113
|
-
'treemap-3x': 'treemap',
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const ALIASES = Object.freeze({ donut: 'doughnut' });
|
|
117
|
-
|
|
118
|
-
// Resolve an OPF chart type id to {id, spec}. Kept and deprecated ids resolve
|
|
119
|
-
// to a kept id; anything else returns the legacy heuristic with id: null.
|
|
79
|
+
// Resolve an OPF chart type id to {id, spec}. `chart.type` is a schema enum in OPF 0.15 (core CHART_TYPES), so every
|
|
80
|
+
// validated document names one of these; an id outside the table (a caller that skipped validation) resolves to
|
|
81
|
+
// {id: null, spec: undefined}.
|
|
120
82
|
export function resolveChartType(type) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (Object.hasOwn(CHART_TYPES, id)) return { id, spec: CHART_TYPES[id] };
|
|
124
|
-
return { id: null, spec: legacyChartType(raw) };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// The pre-FF-22 heuristic for ids outside the core catalog.
|
|
128
|
-
function legacyChartType(normalized) {
|
|
129
|
-
if (normalized.includes('pie')) return CHART_TYPES.pie;
|
|
130
|
-
if (normalized.includes('doughnut') || normalized.includes('donut')) return CHART_TYPES.doughnut;
|
|
131
|
-
if (normalized.includes('area')) return { ...CHART_TYPES.area, legacy: true };
|
|
132
|
-
if (normalized.includes('line')) return { ...CHART_TYPES['line-with-markers'], legacy: true };
|
|
133
|
-
if (normalized.includes('scatter')) return CHART_TYPES.scatter;
|
|
134
|
-
if (normalized.includes('radar')) return { ...CHART_TYPES.radar, legacy: true };
|
|
135
|
-
const grouping = normalized.includes('stacked') ? 'stacked' : 'clustered';
|
|
136
|
-
return category('bar', { barDir: normalized.includes('bar') ? 'bar' : 'col', grouping, legacy: true });
|
|
83
|
+
const id = String(type ?? '').trim();
|
|
84
|
+
return Object.hasOwn(CHART_TYPES, id) ? { id, spec: CHART_TYPES[id] } : { id: null, spec: undefined };
|
|
137
85
|
}
|
|
138
86
|
|
|
139
87
|
// Native construct -> kept OPF id, for import. `node` is the parsed chart-type
|
|
@@ -149,17 +97,17 @@ export function chartTypeFromNative(element, node) {
|
|
|
149
97
|
case 'barChart':
|
|
150
98
|
case 'bar3DChart': {
|
|
151
99
|
const direction = attr('barDir') === 'bar' ? 'bar' : 'column';
|
|
152
|
-
if (percent) return `100pct-stacked-${direction}
|
|
153
|
-
if (stacked) return `stacked-${direction}
|
|
100
|
+
if (percent) return `100pct-stacked-${direction}`;
|
|
101
|
+
if (stacked) return `stacked-${direction}`;
|
|
154
102
|
return direction;
|
|
155
103
|
}
|
|
156
104
|
case 'lineChart':
|
|
157
105
|
case 'line3DChart':
|
|
158
|
-
if (stacked) return markers ? 'stacked-line-with-markers
|
|
106
|
+
if (stacked) return markers ? 'stacked-line-with-markers' : 'stacked-line';
|
|
159
107
|
return markers ? 'line-with-markers' : 'line';
|
|
160
108
|
case 'areaChart':
|
|
161
109
|
case 'area3DChart':
|
|
162
|
-
return percent ? '100pct-stacked-area
|
|
110
|
+
return percent ? '100pct-stacked-area' : stacked ? 'stacked-area' : 'area';
|
|
163
111
|
case 'pieChart':
|
|
164
112
|
case 'pie3DChart':
|
|
165
113
|
case 'ofPieChart':
|
|
@@ -179,6 +127,25 @@ export function chartTypeFromNative(element, node) {
|
|
|
179
127
|
}
|
|
180
128
|
}
|
|
181
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A combo chart in a parsed c:plotArea: exactly one clustered (or standard) column c:barChart and one or two standard
|
|
132
|
+
* c:lineChart groups, and no other chart group. Returns {bar, lines: [{node, secondary}]} (a line group is secondary when it
|
|
133
|
+
* plots against an axis the column group does not use) or null for any other plot area.
|
|
134
|
+
*/
|
|
135
|
+
export function comboFromNative(plotArea) {
|
|
136
|
+
if (!plotArea) return null;
|
|
137
|
+
const bars = asArray(plotArea['c:barChart']);
|
|
138
|
+
const lines = asArray(plotArea['c:lineChart']);
|
|
139
|
+
if (bars.length !== 1 || lines.length < 1 || lines.length > 2) return null;
|
|
140
|
+
if (NATIVE_CHART_ELEMENTS.some((element) => element !== 'barChart' && element !== 'lineChart' && plotArea[`c:${element}`] !== undefined)) return null;
|
|
141
|
+
const [bar] = bars;
|
|
142
|
+
if ((bar['c:barDir']?.val ?? 'col') !== 'col' || !['clustered', 'standard'].includes(bar['c:grouping']?.val ?? 'clustered')) return null;
|
|
143
|
+
if (lines.some((line) => !['standard', undefined].includes(line['c:grouping']?.val))) return null;
|
|
144
|
+
const axes = (node) => asArray(node['c:axId']).map((axis) => String(axis?.val));
|
|
145
|
+
const primary = axes(bar);
|
|
146
|
+
return { bar, lines: lines.map((node) => ({ node, secondary: axes(node).some((id) => !primary.includes(id)) })) };
|
|
147
|
+
}
|
|
148
|
+
|
|
182
149
|
export const NATIVE_CHART_ELEMENTS = Object.freeze([
|
|
183
150
|
'barChart', 'bar3DChart', 'lineChart', 'line3DChart', 'pieChart', 'pie3DChart', 'ofPieChart',
|
|
184
151
|
'doughnutChart', 'areaChart', 'area3DChart', 'scatterChart', 'radarChart',
|
|
@@ -191,6 +158,7 @@ function asArray(value) {
|
|
|
191
158
|
// PptxGenJS omits or cannot express parts of these constructs. Rewrite only
|
|
192
159
|
// the generated chart-type element so the part states the exact grouping.
|
|
193
160
|
export function applyChartConstruct(xml, spec) {
|
|
161
|
+
if (spec?.family === 'combo') return applyComboConstruct(xml);
|
|
194
162
|
// ScatterWithMarkers: the core catalog records scatterStyle "marker" (markers, no connecting line). PptxGenJS writes lineMarker and relies on the series line being noFill.
|
|
195
163
|
if (spec?.family === 'xy') return xml.replace(/<c:scatterStyle val="[^"]*"\/>/, '<c:scatterStyle val="marker"/>');
|
|
196
164
|
if (!spec || spec.family !== 'category') return xml;
|
|
@@ -214,3 +182,21 @@ export function applyChartConstruct(xml, spec) {
|
|
|
214
182
|
}
|
|
215
183
|
return xml.slice(0, start) + open + body + xml.slice(end);
|
|
216
184
|
}
|
|
185
|
+
|
|
186
|
+
// FA-15: PptxGenJS writes each c:lineChart of a multi-type chart without the c:grouping CT_LineChart requires, gives its series
|
|
187
|
+
// the bar-only c:invertIfNegative, and writes c:dLbls before c:marker (CT_LineSer orders marker, dPt, dLbls). Rewrite every line
|
|
188
|
+
// group of a combo chart into schema order; the c:barChart already states barDir and grouping.
|
|
189
|
+
export function applyComboConstruct(xml) {
|
|
190
|
+
return xml.replace(/<c:lineChart>([\s\S]*?)<\/c:lineChart>/g, (_, body) => {
|
|
191
|
+
let next = `<c:grouping val="standard"/>${body.replace(/<c:grouping val="[^"]*"\/>/g, '')}`;
|
|
192
|
+
next = next.replace(/<c:invertIfNegative val="[^"]*"\/>/g, '');
|
|
193
|
+
next = next.replace(/<c:ser>[\s\S]*?<\/c:ser>/g, (ser) => {
|
|
194
|
+
const marker = /<c:marker>[\s\S]*?<\/c:marker>/.exec(ser)?.[0];
|
|
195
|
+
const labels = /<c:dLbls>[\s\S]*?<\/c:dLbls>/.exec(ser);
|
|
196
|
+
if (!marker || !labels || ser.indexOf(marker) < labels.index) return ser;
|
|
197
|
+
const without = ser.replace(marker, '');
|
|
198
|
+
return without.replace(/<c:dLbls>/, `${marker}<c:dLbls>`);
|
|
199
|
+
});
|
|
200
|
+
return `<c:lineChart>${next}</c:lineChart>`;
|
|
201
|
+
});
|
|
202
|
+
}
|
package/dist/code-provenance.js
CHANGED
|
@@ -19,8 +19,8 @@ function unhex(value) {
|
|
|
19
19
|
return JSON.parse(dec.decode(Uint8Array.from(value.match(/../g),byte=>parseInt(byte,16))));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function codeManifest(value, layout, group) {
|
|
23
|
-
return {v:1, group, role:'panel', value, parts:layout.parts.map(part=>({role:part.role, generated:part.generated === true,
|
|
22
|
+
export function codeManifest(value, layout, group, bands) {
|
|
23
|
+
return {v:1, group, role:'panel', value, ...(bands ? {bands} : {}), parts:layout.parts.map(part=>({role:part.role, generated:part.generated === true,
|
|
24
24
|
lines:part.fit.sourceLines.map(({start,end,nextStart,boundary})=>({start,end,nextStart,boundary}))}))};
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -126,11 +126,16 @@ function sourceFor(value, role, generated) {
|
|
|
126
126
|
function validateManifest(manifest) {
|
|
127
127
|
const {value,parts} = manifest;
|
|
128
128
|
const object = value && typeof value === 'object' && !Array.isArray(value);
|
|
129
|
-
if (typeof value !== 'string' && (!object || typeof value.source !== 'string' || Object.keys(value).some(key=>!['source','filename','language'].includes(key)) || ['filename','language'].some(key=>value[key] !== undefined && typeof value[key] !== 'string'))) throw new Error('Invalid source value.');
|
|
129
|
+
if (typeof value !== 'string' && (!object || typeof value.source !== 'string' || Object.keys(value).some(key=>!['source','filename','language','highlight'].includes(key)) || ['filename','language'].some(key=>value[key] !== undefined && typeof value[key] !== 'string'))) throw new Error('Invalid source value.');
|
|
130
|
+
// FA-13: code.highlight is a list of line numbers and [start, end] pairs from 1.
|
|
131
|
+
const line = entry => Number.isSafeInteger(entry) && entry >= 1;
|
|
132
|
+
if (object && value.highlight !== undefined && (!Array.isArray(value.highlight) || !value.highlight.length || !value.highlight.every(entry => line(entry) || (Array.isArray(entry) && entry.length === 2 && entry.every(line))))) throw new Error('Invalid highlight value.');
|
|
130
133
|
// Empty metadata is preserved in the manifest but does not produce a label.
|
|
131
134
|
const roles = [...(object && value.filename ? ['filename'] : []), ...(object && value.language ? ['language'] : []), 'body'];
|
|
132
135
|
if (roles.length === 1) roles.unshift('language');
|
|
133
136
|
if (!Array.isArray(parts) || parts.length !== roles.length) throw new Error('Invalid code parts.');
|
|
137
|
+
// FA-13: the number of native highlight bands the export drew behind the lines.
|
|
138
|
+
if (manifest.bands !== undefined && (!Number.isSafeInteger(manifest.bands) || manifest.bands < 1 || manifest.bands > 100000 || !object || value.highlight === undefined)) throw new Error('Invalid highlight bands.');
|
|
134
139
|
parts.forEach((part,i)=>{
|
|
135
140
|
const generated = roles.length === 2 && roles[0] === 'language' && !(object && (value.filename || value.language)) && i === 0;
|
|
136
141
|
if (part.role !== roles[i] || part.generated !== generated || !Array.isArray(part.lines) || !part.lines.length) throw new Error('Invalid source part.');
|
|
@@ -180,10 +185,21 @@ export function importCodeGroups(shapes, paragraphs, relationships, entries, rep
|
|
|
180
185
|
if (panels.length !== 1 || panels[0].text !== '') throw new Error('Missing, duplicated or edited panel.');
|
|
181
186
|
const panel = panels[0], manifest = panel.data;
|
|
182
187
|
validateManifest(manifest);
|
|
188
|
+
// FA-13: highlight bands are decoration. The panel and every line are required; a band may be missing (someone removed it in
|
|
189
|
+
// PowerPoint) without losing the source, and code.highlight itself comes from the manifest value.
|
|
183
190
|
const expectedCount = 1 + manifest.parts.reduce((sum,part)=>sum+part.lines.length,0);
|
|
184
|
-
|
|
191
|
+
const bandCount = group.filter(item=>item.data.role === 'highlight').length;
|
|
192
|
+
if (group.length !== expectedCount + bandCount || new Set(group.map(item=>item.shape)).size !== group.length) throw new Error('Incomplete or duplicated code shapes.');
|
|
185
193
|
const nativeLines = new Map();
|
|
194
|
+
const seenBands = new Set();
|
|
186
195
|
for (const item of group) if (item !== panel) {
|
|
196
|
+
// FA-13: a highlight band holds no text; it only has to be one of the manifest's bands.
|
|
197
|
+
if (item.data.role === 'highlight') {
|
|
198
|
+
const {index} = item.data;
|
|
199
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= (manifest.bands ?? 0) || seenBands.has(index) || item.text !== '') throw new Error('Ambiguous highlight band.');
|
|
200
|
+
seenBands.add(index);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
187
203
|
const {role,part,line} = item.data, key = `${part}:${line}`;
|
|
188
204
|
if (role !== 'line' || !Number.isSafeInteger(part) || !Number.isSafeInteger(line) || !manifest.parts[part]?.lines[line] || nativeLines.has(key)) throw new Error('Ambiguous source line.');
|
|
189
205
|
nativeLines.set(key,item.text);
|
package/dist/color-ref.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Export ColorRef through published core resolveColorRef(). Keep 8-digit hex
|
|
2
2
|
// alpha locally: normalizeHexColor() strips the AA byte, which PptxGenJS needs.
|
|
3
3
|
|
|
4
|
-
import { resolveColorRef as resolveCoreColorRef } from "@openpresentation/opf";
|
|
4
|
+
import { resolveColorRef as resolveCoreColorRef } from "@openpresentation/opf/composition";
|
|
5
5
|
|
|
6
6
|
const UNRESOLVED = "";
|
|
7
7
|
|
|
@@ -38,14 +38,19 @@ export function resolveVariableColors(variables) {
|
|
|
38
38
|
return isPlainObject(variables) ? variables : {};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// The color roles a ColorRef names. An export context carries them resolved (core resolveColorRoles); a bare
|
|
42
|
+
// context with only `colors` maps those fields (the exporter's colors.accent is the primary color).
|
|
41
43
|
function coreRoles(ctx) {
|
|
42
44
|
const colors = ctx.colors ?? {};
|
|
45
|
+
const roles = ctx.roles;
|
|
43
46
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
primary: withHash(roles?.primary),
|
|
48
|
+
secondary: withHash(roles?.secondary),
|
|
49
|
+
accent: withHash(roles ? roles.accent : colors.accent),
|
|
50
|
+
background: withHash(roles?.background ?? colors.background),
|
|
51
|
+
surface: withHash(roles?.surface ?? colors.surface),
|
|
52
|
+
text: withHash(roles?.text ?? colors.text),
|
|
53
|
+
textSecondary: withHash(roles?.textSecondary ?? colors.mutedText),
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -76,6 +81,7 @@ export function colorContext(context, fallback) {
|
|
|
76
81
|
return {
|
|
77
82
|
colorScheme: context.colorScheme,
|
|
78
83
|
colors: context.colors,
|
|
84
|
+
roles: context.roles,
|
|
79
85
|
variables: context.variables ?? {},
|
|
80
86
|
fallback: fallback ?? context.colors?.text ?? "000000",
|
|
81
87
|
};
|
package/dist/data-provenance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
-
import {
|
|
2
|
+
import {checkFormat, isValidFormat} from './format-check.js';
|
|
3
3
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
4
|
-
import {chartUsesDataFields, isDatasetRef,
|
|
4
|
+
import {chartUsesDataFields, isDatasetRef, fromExcelNumberFormat, resolveTableData, tableCellDisplayValue, tableUsesDataFields} from './chart-data.js';
|
|
5
5
|
|
|
6
6
|
// RR-54: chart and table data survive a PPTX round trip (core docs/chart-table-data.md).
|
|
7
7
|
//
|
|
@@ -13,11 +13,13 @@ import {chartUsesDataFields, isDatasetRef, numberFormatFromExcel, resolveTableDa
|
|
|
13
13
|
//
|
|
14
14
|
// OPF_DATASETS_V1 on p:presentation (beside OPF_DOCUMENT_V1): the whole `datasets` map, unused datasets included.
|
|
15
15
|
// OPF_DATA_V1 on each chart or table graphic frame that uses one of the fields above:
|
|
16
|
-
// chart {v: 1, kind: 'chart', data, mapping?, dataset?: hash, evidence: [hash]} `data` is the authored
|
|
16
|
+
// chart {v: 1, kind: 'chart', data, mapping?, highlight?, dataset?: hash, evidence: [hash]} `data` is the authored
|
|
17
17
|
// chart.data (a dataset reference, or inline columns with DataColumn objects and `source`); `evidence`
|
|
18
18
|
// hashes the native caches (series names, categories, values and format codes) of the chart part(s) the frame
|
|
19
19
|
// shows (the chartex part and its classic fallback); `dataset` hashes the referenced dataset, so a slide
|
|
20
|
-
// pasted into a deck whose dataset of the same id holds other data keeps its values inline.
|
|
20
|
+
// pasted into a deck whose dataset of the same id holds other data keeps its values inline. `highlight` (FA-14) is
|
|
21
|
+
// the authored chart.highlight: the native chart carries only the accent and muted colours it produced, which no
|
|
22
|
+
// exact inference turns back into series and category names, and a chart with only a highlight records its inline `data`.
|
|
21
23
|
// table {v: 1, kind: 'table', table} a dataset-backed table, as authored
|
|
22
24
|
// {v: 1, kind: 'table', headers: [[column, header, text]], cells: [[row, column, value, format, text]]}
|
|
23
25
|
// an inline table: the DataColumn or formatted
|
|
@@ -79,13 +81,13 @@ const canonicalValue = text => NUMBER.test(text.trim()) && Number.isFinite(Numbe
|
|
|
79
81
|
// A General format code is left out: on a plain save PowerPoint writes <c:formatCode>General</c:formatCode> into every
|
|
80
82
|
// cache that has none (PptxGenJS writes none on pie and doughnut series), so General and no code are the same evidence.
|
|
81
83
|
const generalCode = code => /^\s*general\s*$/i.test(code);
|
|
82
|
-
// A format code is hashed in core's canonical form (
|
|
84
|
+
// A format code is hashed in core's canonical form (fromExcelNumberFormat), because a plain PowerPoint save re-spells codes
|
|
83
85
|
// without changing what they show: `$#,##0.0` becomes `\$#,##0.0` and `#,##0 "units"` becomes `#,##0\ "units"` (native
|
|
84
86
|
// check opf#385). Core reads quoted and backslash-escaped literals and literal-safe characters to the same NumberFormat, so
|
|
85
87
|
// both spellings hash equal, while a real change (another number of decimals, another unit) still changes the hash. A code
|
|
86
88
|
// core cannot map is hashed as written; the `opf:` prefix keeps a mapped form apart from a verbatim code of the same text.
|
|
87
89
|
const formatEvidence = code => {
|
|
88
|
-
const format =
|
|
90
|
+
const format = fromExcelNumberFormat(code);
|
|
89
91
|
return typeof format === 'string' ? `format:opf:${format}` : `format:${code}`;
|
|
90
92
|
};
|
|
91
93
|
function hash(text) {
|
|
@@ -119,9 +121,9 @@ const datasetHash = (datasets, id) => own(datasets, id) ? hash(canonical(dataset
|
|
|
119
121
|
|
|
120
122
|
/** The record of a chart that uses an RR-54 data field, or undefined (the frame then carries no tag). */
|
|
121
123
|
export function chartDataRecord(chart, datasets) {
|
|
122
|
-
if (!chartUsesDataFields(chart)) return undefined;
|
|
124
|
+
if (!chartUsesDataFields(chart) && !object(chart?.highlight)) return undefined;
|
|
123
125
|
const dataset = isDatasetRef(chart.data) ? datasetHash(datasets, chart.data.dataset) : undefined;
|
|
124
|
-
return {v: 1, kind: 'chart', data: clone(chart.data), ...(chart.mapping !== undefined ? {mapping: clone(chart.mapping)} : {}), ...(dataset ? {dataset} : {})};
|
|
126
|
+
return {v: 1, kind: 'chart', data: clone(chart.data), ...(chart.mapping !== undefined ? {mapping: clone(chart.mapping)} : {}), ...(object(chart.highlight) ? {highlight: clone(chart.highlight)} : {}), ...(dataset ? {dataset} : {})};
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/** The record of a table that is dataset-backed or formats numbers, or undefined. `layout` is the exported table layout. */
|
|
@@ -281,8 +283,9 @@ export function readDatasetsTag(entries, presentationRoot, relationships, report
|
|
|
281
283
|
const datasets = readTags(entries, presentationRoot?.['p:custDataLst'], relationships, DATASETS_TAG);
|
|
282
284
|
if (datasets === undefined) return undefined;
|
|
283
285
|
if (!object(datasets) || !Object.keys(datasets).length) throw Error('Invalid datasets record.');
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
+
const report = checkFormat({slides: [{title: 'Datasets'}], datasets});
|
|
287
|
+
const [first] = report.findings.filter(finding => finding.severity === 'error');
|
|
288
|
+
if (!report.valid) throw Error(`The stored datasets do not validate (${first?.path ?? ''} ${first?.message ?? ''}).`);
|
|
286
289
|
return datasets;
|
|
287
290
|
} catch (error) {
|
|
288
291
|
report?.({code: 'invalid-data-provenance', path: 'datasets', message: `${error instanceof Error ? error.message : String(error)} The datasets recorded at export were not restored.`});
|
|
@@ -297,6 +300,7 @@ export function readDataTag(entries, frame, relationships) {
|
|
|
297
300
|
if (!object(record) || record.v !== 1 || !['chart', 'table'].includes(record.kind)) throw Error('Unsupported data provenance version.');
|
|
298
301
|
if (record.kind === 'chart' && (!object(record.data) || !Array.isArray(record.evidence) || !record.evidence.every(item => typeof item === 'string'))) throw Error('Invalid chart data record.');
|
|
299
302
|
if (record.kind === 'chart' && record.dataset !== undefined && typeof record.dataset !== 'string') throw Error('Invalid chart data record.');
|
|
303
|
+
if (record.kind === 'chart' && record.highlight !== undefined && !object(record.highlight)) throw Error('Invalid chart data record.');
|
|
300
304
|
if (record.kind === 'table' && !(object(record.table) || (Array.isArray(record.headers) && Array.isArray(record.cells)))) throw Error('Invalid table data record.');
|
|
301
305
|
// Inline table entries are [column, header, text] and [row, column, value, format, text]; their indices are array
|
|
302
306
|
// indices (non-negative integers), never a key such as "__proto__" or "length".
|
|
@@ -309,7 +313,7 @@ export function readDataTag(entries, frame, relationships) {
|
|
|
309
313
|
return record;
|
|
310
314
|
}
|
|
311
315
|
|
|
312
|
-
const validWith = (payload, datasets) =>
|
|
316
|
+
const validWith = (payload, datasets) => isValidFormat({slides: [{title: 'Data', ...payload}], ...(datasets ? {datasets} : {})});
|
|
313
317
|
|
|
314
318
|
/**
|
|
315
319
|
* The authored chart data while the native caches are unchanged: the imported chart (type and options) with the recorded
|
|
@@ -317,7 +321,7 @@ const validWith = (payload, datasets) => validatePresentation({slides: [{title:
|
|
|
317
321
|
*/
|
|
318
322
|
export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
319
323
|
if (!record.evidence.includes(evidence)) {
|
|
320
|
-
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.'});
|
|
324
|
+
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.' + (record.highlight ? ' The chart highlight (series and categories named at export) was not restored either: its names refer to the data as exported.' : '')});
|
|
321
325
|
return chart;
|
|
322
326
|
}
|
|
323
327
|
if (isDatasetRef(record.data) && !own(datasets, record.data.dataset)) {
|
|
@@ -328,8 +332,8 @@ export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
|
328
332
|
report({code: 'chart-dataset-unavailable', message: `The chart referenced dataset '${record.data.dataset}', but this package's dataset of that id holds other data than at export (a slide pasted from another deck); the chart imports its values inline.`});
|
|
329
333
|
return chart;
|
|
330
334
|
}
|
|
331
|
-
const {mapping: _mapping, ...rest} = chart;
|
|
332
|
-
const restored = {...rest, data: clone(record.data), ...(record.mapping !== undefined ? {mapping: clone(record.mapping)} : {})};
|
|
335
|
+
const {mapping: _mapping, highlight: _highlight, ...rest} = chart;
|
|
336
|
+
const restored = {...rest, data: clone(record.data), ...(record.mapping !== undefined ? {mapping: clone(record.mapping)} : {}), ...(record.highlight !== undefined ? {highlight: clone(record.highlight)} : {})};
|
|
333
337
|
if (!validWith({chart: restored}, isDatasetRef(record.data) ? datasets : undefined)) {
|
|
334
338
|
report({code: 'invalid-data-provenance', message: 'The chart data recorded at export does not form a valid chart; the chart imports the values it shows.'});
|
|
335
339
|
return chart;
|