@qvac/harness 0.7.3 → 0.8.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/CHANGELOG.md +45 -1
- package/NOTICE.md +16 -0
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +28 -8
- package/dist-lib/lib/capabilities/tool-turns.d.ts +3 -0
- package/dist-lib/lib/capabilities/tool-turns.js +7 -0
- package/dist-lib/lib/context-window.js +2 -2
- package/dist-lib/lib/helpers/run-outcome.d.ts +4 -1
- package/dist-lib/lib/helpers/run-outcome.js +14 -8
- package/dist-lib/lib/helpers/tool-call-markup.js +4 -4
- package/dist-lib/lib/hooks/built-in/arg-repair.js +7 -0
- package/dist-lib/lib/office/engine/codes.d.ts +62 -0
- package/dist-lib/lib/office/engine/codes.js +61 -0
- package/dist-lib/lib/office/engine/docx/document.d.ts +216 -0
- package/dist-lib/lib/office/engine/docx/document.js +1152 -0
- package/dist-lib/lib/office/engine/docx/index.d.ts +7 -0
- package/dist-lib/lib/office/engine/docx/index.js +8 -0
- package/dist-lib/lib/office/engine/docx/styles.d.ts +31 -0
- package/dist-lib/lib/office/engine/docx/styles.js +173 -0
- package/dist-lib/lib/office/engine/docx/template.d.ts +18 -0
- package/dist-lib/lib/office/engine/docx/template.js +133 -0
- package/dist-lib/lib/office/engine/docx/text.d.ts +55 -0
- package/dist-lib/lib/office/engine/docx/text.js +538 -0
- package/dist-lib/lib/office/engine/drawingml/chart.d.ts +63 -0
- package/dist-lib/lib/office/engine/drawingml/chart.js +539 -0
- package/dist-lib/lib/office/engine/drawingml/text.d.ts +35 -0
- package/dist-lib/lib/office/engine/drawingml/text.js +352 -0
- package/dist-lib/lib/office/engine/errors.d.ts +6 -0
- package/dist-lib/lib/office/engine/errors.js +12 -0
- package/dist-lib/lib/office/engine/image.d.ts +8 -0
- package/dist-lib/lib/office/engine/image.js +48 -0
- package/dist-lib/lib/office/engine/index.d.ts +12 -0
- package/dist-lib/lib/office/engine/index.js +7 -0
- package/dist-lib/lib/office/engine/opc/content-types.d.ts +17 -0
- package/dist-lib/lib/office/engine/opc/content-types.js +74 -0
- package/dist-lib/lib/office/engine/opc/media.d.ts +13 -0
- package/dist-lib/lib/office/engine/opc/media.js +11 -0
- package/dist-lib/lib/office/engine/opc/package.d.ts +51 -0
- package/dist-lib/lib/office/engine/opc/package.js +324 -0
- package/dist-lib/lib/office/engine/opc/paths.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/paths.js +46 -0
- package/dist-lib/lib/office/engine/opc/properties.d.ts +7 -0
- package/dist-lib/lib/office/engine/opc/properties.js +75 -0
- package/dist-lib/lib/office/engine/opc/relationships.d.ts +23 -0
- package/dist-lib/lib/office/engine/opc/relationships.js +65 -0
- package/dist-lib/lib/office/engine/opc/zip.d.ts +15 -0
- package/dist-lib/lib/office/engine/opc/zip.js +264 -0
- package/dist-lib/lib/office/engine/pptx/fit.d.ts +12 -0
- package/dist-lib/lib/office/engine/pptx/fit.js +37 -0
- package/dist-lib/lib/office/engine/pptx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/pptx/index.js +8 -0
- package/dist-lib/lib/office/engine/pptx/inherit.d.ts +29 -0
- package/dist-lib/lib/office/engine/pptx/inherit.js +155 -0
- package/dist-lib/lib/office/engine/pptx/notes.d.ts +5 -0
- package/dist-lib/lib/office/engine/pptx/notes.js +85 -0
- package/dist-lib/lib/office/engine/pptx/presentation.d.ts +54 -0
- package/dist-lib/lib/office/engine/pptx/presentation.js +749 -0
- package/dist-lib/lib/office/engine/pptx/read.d.ts +9 -0
- package/dist-lib/lib/office/engine/pptx/read.js +66 -0
- package/dist-lib/lib/office/engine/pptx/shapes.d.ts +52 -0
- package/dist-lib/lib/office/engine/pptx/shapes.js +276 -0
- package/dist-lib/lib/office/engine/pptx/slides.d.ts +27 -0
- package/dist-lib/lib/office/engine/pptx/slides.js +123 -0
- package/dist-lib/lib/office/engine/pptx/template.d.ts +20 -0
- package/dist-lib/lib/office/engine/pptx/template.js +391 -0
- package/dist-lib/lib/office/engine/pptx/types.d.ts +120 -0
- package/dist-lib/lib/office/engine/pptx/types.js +1 -0
- package/dist-lib/lib/office/engine/units.d.ts +3 -0
- package/dist-lib/lib/office/engine/units.js +12 -0
- package/dist-lib/lib/office/engine/utf8.d.ts +2 -0
- package/dist-lib/lib/office/engine/utf8.js +103 -0
- package/dist-lib/lib/office/engine/xlsx/formula.d.ts +49 -0
- package/dist-lib/lib/office/engine/xlsx/formula.js +381 -0
- package/dist-lib/lib/office/engine/xlsx/index.d.ts +8 -0
- package/dist-lib/lib/office/engine/xlsx/index.js +8 -0
- package/dist-lib/lib/office/engine/xlsx/refs.d.ts +22 -0
- package/dist-lib/lib/office/engine/xlsx/refs.js +82 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.d.ts +21 -0
- package/dist-lib/lib/office/engine/xlsx/reshape.js +424 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.d.ts +108 -0
- package/dist-lib/lib/office/engine/xlsx/sheet.js +662 -0
- package/dist-lib/lib/office/engine/xlsx/strings.d.ts +15 -0
- package/dist-lib/lib/office/engine/xlsx/strings.js +93 -0
- package/dist-lib/lib/office/engine/xlsx/styles.d.ts +38 -0
- package/dist-lib/lib/office/engine/xlsx/styles.js +369 -0
- package/dist-lib/lib/office/engine/xlsx/template.d.ts +7 -0
- package/dist-lib/lib/office/engine/xlsx/template.js +54 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +161 -0
- package/dist-lib/lib/office/engine/xlsx/workbook.js +850 -0
- package/dist-lib/lib/office/engine/xlsx/write.d.ts +2 -0
- package/dist-lib/lib/office/engine/xlsx/write.js +42 -0
- package/dist-lib/lib/office/engine/xml.d.ts +45 -0
- package/dist-lib/lib/office/engine/xml.js +258 -0
- package/dist-lib/lib/tools/exec/scope.d.ts +2 -0
- package/dist-lib/lib/tools/exec/scope.js +33 -2
- package/dist-lib/lib/tools/pdf/build.d.ts +43 -0
- package/dist-lib/lib/tools/pdf/build.js +150 -0
- package/dist-lib/lib/tools/pdf/images.d.ts +7 -0
- package/dist-lib/lib/tools/pdf/images.js +82 -0
- package/dist-lib/lib/tools/pdf/request.d.ts +63 -0
- package/dist-lib/lib/tools/pdf/request.js +335 -0
- package/dist-lib/lib/tools/pdf.d.ts +6 -0
- package/dist-lib/lib/tools/pdf.js +302 -0
- package/dist-lib/lib/tools/skill.js +14 -0
- package/package.json +25 -1
- package/worker/harness-sidecar.mjs +3 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import * as xml from '../xml.js';
|
|
2
|
+
import * as paths from '../opc/paths.js';
|
|
3
|
+
import * as text from './text.js';
|
|
4
|
+
import * as refs from '../xlsx/refs.js';
|
|
5
|
+
import { writeWorkbook } from '../xlsx/write.js';
|
|
6
|
+
import { DocsError } from '../errors.js';
|
|
7
|
+
import { isRecord } from '../../../guards.js';
|
|
8
|
+
export const C = xml.ns.c;
|
|
9
|
+
export const CHART_TYPE = 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml';
|
|
10
|
+
export const GRAPHIC_URI = 'http://schemas.openxmlformats.org/drawingml/2006/chart';
|
|
11
|
+
export const CREATABLE = ['column', 'bar', 'line', 'pie', 'doughnut', 'area'];
|
|
12
|
+
const A = xml.ns.a;
|
|
13
|
+
const R = xml.ns.r;
|
|
14
|
+
const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
15
|
+
const REL_PACKAGE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/package';
|
|
16
|
+
const SHEET = 'Sheet1';
|
|
17
|
+
const LEGEND_POSITIONS = { right: 'r', left: 'l', top: 't', bottom: 'b' };
|
|
18
|
+
const EDITABLE = new Set([
|
|
19
|
+
'barChart',
|
|
20
|
+
'bar3DChart',
|
|
21
|
+
'lineChart',
|
|
22
|
+
'line3DChart',
|
|
23
|
+
'pieChart',
|
|
24
|
+
'pie3DChart',
|
|
25
|
+
'doughnutChart',
|
|
26
|
+
'areaChart',
|
|
27
|
+
'area3DChart',
|
|
28
|
+
'radarChart'
|
|
29
|
+
]);
|
|
30
|
+
const SERIES_ORDER = [
|
|
31
|
+
'idx',
|
|
32
|
+
'order',
|
|
33
|
+
'tx',
|
|
34
|
+
'spPr',
|
|
35
|
+
'invertIfNegative',
|
|
36
|
+
'pictureOptions',
|
|
37
|
+
'marker',
|
|
38
|
+
'explosion',
|
|
39
|
+
'dPt',
|
|
40
|
+
'dLbls',
|
|
41
|
+
'trendline',
|
|
42
|
+
'errBars',
|
|
43
|
+
'cat',
|
|
44
|
+
'val',
|
|
45
|
+
'smooth',
|
|
46
|
+
'shape',
|
|
47
|
+
'bubble3D',
|
|
48
|
+
'extLst'
|
|
49
|
+
];
|
|
50
|
+
const CHART_ORDER = [
|
|
51
|
+
'title',
|
|
52
|
+
'autoTitleDeleted',
|
|
53
|
+
'pivotFmts',
|
|
54
|
+
'view3D',
|
|
55
|
+
'floor',
|
|
56
|
+
'sideWall',
|
|
57
|
+
'backWall',
|
|
58
|
+
'plotArea',
|
|
59
|
+
'legend',
|
|
60
|
+
'plotVisOnly',
|
|
61
|
+
'dispBlanksAs',
|
|
62
|
+
'showDLblsOverMax',
|
|
63
|
+
'extLst'
|
|
64
|
+
];
|
|
65
|
+
const SPACE_ORDER = [
|
|
66
|
+
'date1904',
|
|
67
|
+
'lang',
|
|
68
|
+
'roundedCorners',
|
|
69
|
+
'AlternateContent',
|
|
70
|
+
'style',
|
|
71
|
+
'clrMapOvr',
|
|
72
|
+
'pivotSource',
|
|
73
|
+
'protection',
|
|
74
|
+
'chart',
|
|
75
|
+
'spPr',
|
|
76
|
+
'txPr',
|
|
77
|
+
'externalData',
|
|
78
|
+
'printSettings',
|
|
79
|
+
'userShapes',
|
|
80
|
+
'extLst'
|
|
81
|
+
];
|
|
82
|
+
function groupsOf(doc) {
|
|
83
|
+
const plotArea = xml.descendants(doc, C, 'plotArea')[0];
|
|
84
|
+
if (!plotArea) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return xml.children(plotArea, C).filter((el) => (el.localName ?? '').endsWith('Chart'));
|
|
88
|
+
}
|
|
89
|
+
function typeOf(group) {
|
|
90
|
+
const name = group.localName ?? '';
|
|
91
|
+
if (name === 'barChart' || name === 'bar3DChart') {
|
|
92
|
+
return xml.child(group, C, 'barDir')?.getAttribute('val') === 'bar' ? 'bar' : 'column';
|
|
93
|
+
}
|
|
94
|
+
if (name === 'ofPieChart') {
|
|
95
|
+
return 'pie-of-pie';
|
|
96
|
+
}
|
|
97
|
+
return name.replace(/3D/, '').replace(/Chart$/, '');
|
|
98
|
+
}
|
|
99
|
+
function seriesOf(doc) {
|
|
100
|
+
return groupsOf(doc).flatMap((group) => xml.children(group, C, 'ser'));
|
|
101
|
+
}
|
|
102
|
+
function cacheOf(container) {
|
|
103
|
+
for (const local of ['strCache', 'numCache', 'strLit', 'numLit', 'lvl']) {
|
|
104
|
+
const found = xml.descendants(container, C, local)[0];
|
|
105
|
+
if (found) {
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
function points(container) {
|
|
112
|
+
if (!container)
|
|
113
|
+
return [];
|
|
114
|
+
const cache = cacheOf(container);
|
|
115
|
+
if (!cache) {
|
|
116
|
+
const v = xml.child(container, C, 'v');
|
|
117
|
+
return v ? [xml.text(v)] : [];
|
|
118
|
+
}
|
|
119
|
+
const numeric = cache.localName === 'numCache' || cache.localName === 'numLit';
|
|
120
|
+
const out = new Array(Number(xml.child(cache, C, 'ptCount')?.getAttribute('val')) || 0).fill(null);
|
|
121
|
+
for (const pt of xml.children(cache, C, 'pt')) {
|
|
122
|
+
const idx = Number(pt.getAttribute('idx'));
|
|
123
|
+
const v = xml.child(pt, C, 'v');
|
|
124
|
+
if (!v || !(idx >= 0)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
while (out.length <= idx) {
|
|
128
|
+
out.push(null);
|
|
129
|
+
}
|
|
130
|
+
out[idx] = numeric ? Number(xml.text(v)) : xml.text(v);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function readTitle(doc) {
|
|
135
|
+
const chart = xml.child(xml.root(doc), C, 'chart');
|
|
136
|
+
const title = chart ? xml.child(chart, C, 'title') : null;
|
|
137
|
+
const tx = title ? xml.child(title, C, 'tx') : null;
|
|
138
|
+
if (!tx) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const rich = xml.child(tx, C, 'rich');
|
|
142
|
+
if (rich) {
|
|
143
|
+
return text.readText(rich);
|
|
144
|
+
}
|
|
145
|
+
const name = points(tx);
|
|
146
|
+
return name.length ? String(name[0]) : null;
|
|
147
|
+
}
|
|
148
|
+
function nameOf(ser) {
|
|
149
|
+
const first = points(xml.child(ser, C, 'tx'))[0];
|
|
150
|
+
return first === undefined || first === null ? null : String(first);
|
|
151
|
+
}
|
|
152
|
+
export function readChart(doc) {
|
|
153
|
+
const groups = groupsOf(doc);
|
|
154
|
+
const sers = seriesOf(doc);
|
|
155
|
+
const first = sers[0];
|
|
156
|
+
let type = null;
|
|
157
|
+
if (groups.length === 1) {
|
|
158
|
+
type = typeOf(groups[0]);
|
|
159
|
+
}
|
|
160
|
+
else if (groups.length > 1) {
|
|
161
|
+
type = 'combo';
|
|
162
|
+
}
|
|
163
|
+
const out = {
|
|
164
|
+
type,
|
|
165
|
+
title: readTitle(doc),
|
|
166
|
+
categories: first ? points(xml.child(first, C, 'cat') ?? xml.child(first, C, 'xVal')) : [],
|
|
167
|
+
series: sers.map((ser) => ({
|
|
168
|
+
name: nameOf(ser),
|
|
169
|
+
values: points(xml.child(ser, C, 'val') ?? xml.child(ser, C, 'yVal'))
|
|
170
|
+
}))
|
|
171
|
+
};
|
|
172
|
+
if (groups.length > 1) {
|
|
173
|
+
out.types = groups.map(typeOf);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
function isRecordLike(value) {
|
|
178
|
+
return isRecord(value) && !Array.isArray(value);
|
|
179
|
+
}
|
|
180
|
+
function validateCategories(categories) {
|
|
181
|
+
if (!Array.isArray(categories) || categories.length === 0) {
|
|
182
|
+
throw new DocsError('INVALID_CHART_DATA', 'categories must be a non-empty array');
|
|
183
|
+
}
|
|
184
|
+
if (categories.length > 10000) {
|
|
185
|
+
throw new DocsError('INVALID_CHART_DATA', 'a chart holds at most 10000 categories');
|
|
186
|
+
}
|
|
187
|
+
return categories.map((c, i) => {
|
|
188
|
+
if (typeof c === 'string' || (typeof c === 'number' && Number.isFinite(c))) {
|
|
189
|
+
return c;
|
|
190
|
+
}
|
|
191
|
+
throw new DocsError('INVALID_CHART_DATA', `category ${i} must be a string or a number`);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function validateSeries(s, i, count) {
|
|
195
|
+
if (!isRecordLike(s) || typeof s.name !== 'string' || !Array.isArray(s.values)) {
|
|
196
|
+
throw new DocsError('INVALID_CHART_DATA', `series ${i} must be { name, values }`);
|
|
197
|
+
}
|
|
198
|
+
const name = s.name;
|
|
199
|
+
if (s.values.length !== count) {
|
|
200
|
+
throw new DocsError('INVALID_CHART_DATA', `series ${i} ("${name}") has ${s.values.length} values but there are ${count} categories`);
|
|
201
|
+
}
|
|
202
|
+
const values = s.values.map((v, j) => {
|
|
203
|
+
if (v === null || (typeof v === 'number' && Number.isFinite(v)))
|
|
204
|
+
return v;
|
|
205
|
+
throw new DocsError('INVALID_CHART_DATA', `value ${j} of series ${i} must be a number or null`);
|
|
206
|
+
});
|
|
207
|
+
return { name, values };
|
|
208
|
+
}
|
|
209
|
+
export function validateData(data) {
|
|
210
|
+
if (!isRecordLike(data)) {
|
|
211
|
+
throw new DocsError('INVALID_CHART_DATA', 'chart data must be { categories, series }');
|
|
212
|
+
}
|
|
213
|
+
const categories = validateCategories(data.categories);
|
|
214
|
+
const { series } = data;
|
|
215
|
+
if (!Array.isArray(series) || series.length === 0) {
|
|
216
|
+
throw new DocsError('INVALID_CHART_DATA', 'series must be a non-empty array of { name, values }');
|
|
217
|
+
}
|
|
218
|
+
if (series.length > 255) {
|
|
219
|
+
throw new DocsError('INVALID_CHART_DATA', 'a chart holds at most 255 series');
|
|
220
|
+
}
|
|
221
|
+
return { categories, series: series.map((s, i) => validateSeries(s, i, categories.length)) };
|
|
222
|
+
}
|
|
223
|
+
function valEl(c, local, value) {
|
|
224
|
+
return c(local, { val: value });
|
|
225
|
+
}
|
|
226
|
+
function textEl(c, local, value) {
|
|
227
|
+
const el = c(local);
|
|
228
|
+
xml.setText(el, value);
|
|
229
|
+
return el;
|
|
230
|
+
}
|
|
231
|
+
function cacheEl(c, local, cached) {
|
|
232
|
+
const cache = c(local);
|
|
233
|
+
if (cached.formatCode !== undefined) {
|
|
234
|
+
cache.appendChild(textEl(c, 'formatCode', cached.formatCode));
|
|
235
|
+
}
|
|
236
|
+
cache.appendChild(valEl(c, 'ptCount', cached.values.length));
|
|
237
|
+
cached.values.forEach((value, idx) => {
|
|
238
|
+
if (value === null || value === undefined)
|
|
239
|
+
return;
|
|
240
|
+
xml.append(cache, c('pt', { idx })).appendChild(textEl(c, 'v', String(value)));
|
|
241
|
+
});
|
|
242
|
+
return cache;
|
|
243
|
+
}
|
|
244
|
+
function refEl(c, local, ref) {
|
|
245
|
+
const el = c(local);
|
|
246
|
+
el.appendChild(textEl(c, 'f', ref.formula));
|
|
247
|
+
el.appendChild(ref.cache);
|
|
248
|
+
return el;
|
|
249
|
+
}
|
|
250
|
+
function replaceChild(ser, local, fill) {
|
|
251
|
+
const c = xml.builder(xml.owner(ser), C);
|
|
252
|
+
const container = xml.child(ser, C, local) ?? xml.insertOrdered(ser, c(local), SERIES_ORDER);
|
|
253
|
+
const previous = xml.descendants(container, C, 'formatCode')[0];
|
|
254
|
+
while (container.firstChild) {
|
|
255
|
+
container.removeChild(container.firstChild);
|
|
256
|
+
}
|
|
257
|
+
fill(container, previous ? xml.text(previous) : 'General');
|
|
258
|
+
}
|
|
259
|
+
function formulas(write) {
|
|
260
|
+
const count = write.data.categories.length;
|
|
261
|
+
const link = write.links?.series[write.index];
|
|
262
|
+
if (write.links && link) {
|
|
263
|
+
return { name: link.name, values: link.values, categories: write.links.categories };
|
|
264
|
+
}
|
|
265
|
+
const col = write.index + 1;
|
|
266
|
+
return {
|
|
267
|
+
name: refs.absolute(SHEET, { top: 0, left: col, bottom: 0, right: col }),
|
|
268
|
+
values: refs.absolute(SHEET, { top: 1, left: col, bottom: count, right: col }),
|
|
269
|
+
categories: refs.absolute(SHEET, { top: 1, left: 0, bottom: count, right: 0 })
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function writeSeries(ser, write) {
|
|
273
|
+
const c = xml.builder(xml.owner(ser), C);
|
|
274
|
+
const { data, index } = write;
|
|
275
|
+
const series = data.series[index];
|
|
276
|
+
const f = formulas(write);
|
|
277
|
+
replaceChild(ser, 'tx', (tx) => {
|
|
278
|
+
const name = f.name;
|
|
279
|
+
if (name) {
|
|
280
|
+
tx.appendChild(refEl(c, 'strRef', {
|
|
281
|
+
formula: name,
|
|
282
|
+
cache: cacheEl(c, 'strCache', { values: [series.name] })
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
tx.appendChild(textEl(c, 'v', series.name));
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const numeric = data.categories.every((cat) => typeof cat === 'number');
|
|
290
|
+
replaceChild(ser, 'cat', (cat, formatCode) => {
|
|
291
|
+
const cache = numeric
|
|
292
|
+
? cacheEl(c, 'numCache', { values: data.categories, formatCode })
|
|
293
|
+
: cacheEl(c, 'strCache', { values: data.categories.map(String) });
|
|
294
|
+
cat.appendChild(refEl(c, numeric ? 'numRef' : 'strRef', { formula: f.categories, cache }));
|
|
295
|
+
});
|
|
296
|
+
replaceChild(ser, 'val', (val, formatCode) => {
|
|
297
|
+
val.appendChild(refEl(c, 'numRef', {
|
|
298
|
+
formula: f.values,
|
|
299
|
+
cache: cacheEl(c, 'numCache', { values: series.values, formatCode })
|
|
300
|
+
}));
|
|
301
|
+
});
|
|
302
|
+
for (const dPt of xml.children(ser, C, 'dPt')) {
|
|
303
|
+
if (Number(xml.child(dPt, C, 'idx')?.getAttribute('val')) >= data.categories.length) {
|
|
304
|
+
xml.remove(dPt);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function setIndex(ser, i) {
|
|
309
|
+
for (const local of ['idx', 'order']) {
|
|
310
|
+
xml.child(ser, C, local)?.setAttribute('val', String(i));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function newSeries(doc, group) {
|
|
314
|
+
const c = xml.builder(doc, C);
|
|
315
|
+
const ser = c('ser');
|
|
316
|
+
ser.appendChild(valEl(c, 'idx', 0));
|
|
317
|
+
ser.appendChild(valEl(c, 'order', 0));
|
|
318
|
+
if (group.localName === 'barChart') {
|
|
319
|
+
ser.appendChild(valEl(c, 'invertIfNegative', 0));
|
|
320
|
+
}
|
|
321
|
+
if (group.localName === 'lineChart') {
|
|
322
|
+
ser.appendChild(valEl(c, 'smooth', 0));
|
|
323
|
+
}
|
|
324
|
+
return ser;
|
|
325
|
+
}
|
|
326
|
+
function insertSeries(group, ser) {
|
|
327
|
+
const existing = xml.children(group, C, 'ser');
|
|
328
|
+
const after = existing.length
|
|
329
|
+
? existing[existing.length - 1]
|
|
330
|
+
: (xml.child(group, C, 'varyColors') ?? xml.child(group, C, 'grouping'));
|
|
331
|
+
group.insertBefore(ser, after ? after.nextSibling : group.firstChild);
|
|
332
|
+
}
|
|
333
|
+
function embeddedRows(data) {
|
|
334
|
+
return [
|
|
335
|
+
['', ...data.series.map((s) => s.name)],
|
|
336
|
+
...data.categories.map((cat, i) => [cat, ...data.series.map((s) => s.values[i])])
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
function writeEmbedding(ref, data) {
|
|
340
|
+
const { pkg, part } = ref;
|
|
341
|
+
const top = pkg.root(part);
|
|
342
|
+
const bytes = writeWorkbook(embeddedRows(data), SHEET);
|
|
343
|
+
const rels = pkg.rels(part);
|
|
344
|
+
let external = xml.child(top, C, 'externalData');
|
|
345
|
+
const current = external ? rels.get(external.getAttributeNS(R, 'id')) : null;
|
|
346
|
+
if (current &&
|
|
347
|
+
!current.external &&
|
|
348
|
+
current.part &&
|
|
349
|
+
pkg.has(current.part) &&
|
|
350
|
+
current.part.endsWith('.xlsx')) {
|
|
351
|
+
pkg.write(current.part, bytes);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (current) {
|
|
355
|
+
rels.remove(current.id);
|
|
356
|
+
}
|
|
357
|
+
pkg.contentTypes.addDefault('xlsx', XLSX_TYPE);
|
|
358
|
+
const embedding = pkg.freeName(paths.resolve(part, '../embeddings/Microsoft_Excel_Worksheet%d.xlsx'));
|
|
359
|
+
pkg.add(embedding, bytes);
|
|
360
|
+
const rId = rels.add(REL_PACKAGE, embedding);
|
|
361
|
+
if (!external) {
|
|
362
|
+
const c = xml.builder(xml.owner(top), C);
|
|
363
|
+
external = xml.insertOrdered(top, c('externalData'), SPACE_ORDER);
|
|
364
|
+
external.appendChild(valEl(c, 'autoUpdate', 0));
|
|
365
|
+
}
|
|
366
|
+
xml.setNsAttribute(external, { namespace: R, local: 'id' }, rId);
|
|
367
|
+
}
|
|
368
|
+
function writeCombo(sers, data, opts) {
|
|
369
|
+
if (data.series.length !== sers.length) {
|
|
370
|
+
throw new DocsError('CHART_COMBO', `this combo chart has ${sers.length} series; give exactly ${sers.length} to keep each on its plot`);
|
|
371
|
+
}
|
|
372
|
+
sers.forEach((ser, index) => writeSeries(ser, { index, data, links: opts.links }));
|
|
373
|
+
}
|
|
374
|
+
function writeSingle(group, data, opts) {
|
|
375
|
+
const doc = xml.owner(group);
|
|
376
|
+
const sers = xml.children(group, C, 'ser');
|
|
377
|
+
for (const extra of sers.splice(data.series.length)) {
|
|
378
|
+
xml.remove(extra);
|
|
379
|
+
}
|
|
380
|
+
while (sers.length < data.series.length) {
|
|
381
|
+
const template = sers[sers.length - 1];
|
|
382
|
+
const ser = template ? xml.clone(template) : newSeries(doc, group);
|
|
383
|
+
for (const local of ['spPr', 'dPt', 'trendline', 'errBars']) {
|
|
384
|
+
for (const el of xml.children(ser, C, local)) {
|
|
385
|
+
xml.remove(el);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
insertSeries(group, ser);
|
|
389
|
+
sers.push(ser);
|
|
390
|
+
}
|
|
391
|
+
sers.forEach((ser, index) => {
|
|
392
|
+
setIndex(ser, index);
|
|
393
|
+
writeSeries(ser, { index, data, links: opts.links });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
export function setChartData(ref, input, opts = {}) {
|
|
397
|
+
const data = validateData(input);
|
|
398
|
+
const doc = ref.pkg.xml(ref.part);
|
|
399
|
+
const groups = groupsOf(doc);
|
|
400
|
+
if (groups.length === 0) {
|
|
401
|
+
throw new DocsError('UNSUPPORTED_CHART', 'the chart has no plot to write data into');
|
|
402
|
+
}
|
|
403
|
+
const unsupported = groups.find((g) => !EDITABLE.has(g.localName ?? ''));
|
|
404
|
+
if (unsupported) {
|
|
405
|
+
throw new DocsError('UNSUPPORTED_CHART', `${typeOf(unsupported)} charts cannot be edited; column, bar, line, pie, doughnut, area and radar charts can`);
|
|
406
|
+
}
|
|
407
|
+
if (groups.length > 1) {
|
|
408
|
+
writeCombo(seriesOf(doc), data, opts);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
writeSingle(groups[0], data, opts);
|
|
412
|
+
}
|
|
413
|
+
if (!opts.links) {
|
|
414
|
+
writeEmbedding(ref, data);
|
|
415
|
+
}
|
|
416
|
+
ref.pkg.touch(ref.part);
|
|
417
|
+
return { series: data.series.length, categories: data.categories.length };
|
|
418
|
+
}
|
|
419
|
+
function richTitle(title) {
|
|
420
|
+
const doc = xml.owner(title);
|
|
421
|
+
const c = xml.builder(doc, C);
|
|
422
|
+
const a = xml.builder(doc, A);
|
|
423
|
+
const tx = xml.child(title, C, 'tx');
|
|
424
|
+
const existing = tx ? xml.child(tx, C, 'rich') : null;
|
|
425
|
+
if (existing) {
|
|
426
|
+
return existing;
|
|
427
|
+
}
|
|
428
|
+
if (tx) {
|
|
429
|
+
xml.remove(tx);
|
|
430
|
+
}
|
|
431
|
+
const fresh = c('tx');
|
|
432
|
+
title.insertBefore(fresh, title.firstChild);
|
|
433
|
+
const rich = xml.append(fresh, c('rich'));
|
|
434
|
+
for (const local of ['bodyPr', 'lstStyle', 'p']) {
|
|
435
|
+
rich.appendChild(a(local));
|
|
436
|
+
}
|
|
437
|
+
return rich;
|
|
438
|
+
}
|
|
439
|
+
export function setChartTitle(ref, value) {
|
|
440
|
+
const chart = xml.child(ref.pkg.root(ref.part), C, 'chart');
|
|
441
|
+
if (!chart) {
|
|
442
|
+
throw new DocsError('UNSUPPORTED_CHART', 'the chart part has no chart');
|
|
443
|
+
}
|
|
444
|
+
const c = xml.builder(xml.owner(chart), C);
|
|
445
|
+
const deleted = xml.child(chart, C, 'autoTitleDeleted') ??
|
|
446
|
+
xml.insertOrdered(chart, valEl(c, 'autoTitleDeleted', 0), CHART_ORDER);
|
|
447
|
+
let title = xml.child(chart, C, 'title');
|
|
448
|
+
if (value === null || value === '') {
|
|
449
|
+
if (title) {
|
|
450
|
+
xml.remove(title);
|
|
451
|
+
}
|
|
452
|
+
deleted.setAttribute('val', '1');
|
|
453
|
+
ref.pkg.touch(ref.part);
|
|
454
|
+
return { changed: true };
|
|
455
|
+
}
|
|
456
|
+
if (!title) {
|
|
457
|
+
title = xml.insertOrdered(chart, c('title'), CHART_ORDER);
|
|
458
|
+
title.appendChild(valEl(c, 'overlay', 0));
|
|
459
|
+
}
|
|
460
|
+
text.writeParagraphs(richTitle(title), value);
|
|
461
|
+
deleted.setAttribute('val', '0');
|
|
462
|
+
ref.pkg.touch(ref.part);
|
|
463
|
+
return { changed: true };
|
|
464
|
+
}
|
|
465
|
+
function isChartType(value) {
|
|
466
|
+
return typeof value === 'string' && CREATABLE.some((type) => type === value);
|
|
467
|
+
}
|
|
468
|
+
function isLegend(value) {
|
|
469
|
+
return value === false || (typeof value === 'string' && value in LEGEND_POSITIONS);
|
|
470
|
+
}
|
|
471
|
+
function groupingFor(type, stacked) {
|
|
472
|
+
if (stacked === 'percent')
|
|
473
|
+
return 'percentStacked';
|
|
474
|
+
if (stacked)
|
|
475
|
+
return 'stacked';
|
|
476
|
+
return type === 'column' || type === 'bar' ? 'clustered' : 'standard';
|
|
477
|
+
}
|
|
478
|
+
function plotXml(type, stacked) {
|
|
479
|
+
const grouping = groupingFor(type, stacked);
|
|
480
|
+
const axes = '<c:axId val="10"/><c:axId val="20"/>';
|
|
481
|
+
if (type === 'column' || type === 'bar') {
|
|
482
|
+
const overlap = stacked ? '<c:overlap val="100"/>' : '';
|
|
483
|
+
return `<c:barChart><c:barDir val="${type === 'bar' ? 'bar' : 'col'}"/><c:grouping val="${grouping}"/><c:varyColors val="0"/><c:gapWidth val="150"/>${overlap}${axes}</c:barChart>`;
|
|
484
|
+
}
|
|
485
|
+
if (type === 'line') {
|
|
486
|
+
return `<c:lineChart><c:grouping val="${grouping}"/><c:varyColors val="0"/><c:marker val="1"/>${axes}</c:lineChart>`;
|
|
487
|
+
}
|
|
488
|
+
if (type === 'area') {
|
|
489
|
+
return `<c:areaChart><c:grouping val="${grouping}"/><c:varyColors val="0"/>${axes}</c:areaChart>`;
|
|
490
|
+
}
|
|
491
|
+
if (type === 'pie') {
|
|
492
|
+
return '<c:pieChart><c:varyColors val="1"/><c:firstSliceAng val="0"/></c:pieChart>';
|
|
493
|
+
}
|
|
494
|
+
return '<c:doughnutChart><c:varyColors val="1"/><c:firstSliceAng val="0"/><c:holeSize val="50"/></c:doughnutChart>';
|
|
495
|
+
}
|
|
496
|
+
function axesXml(type) {
|
|
497
|
+
if (!['column', 'bar', 'line', 'area'].includes(type))
|
|
498
|
+
return '';
|
|
499
|
+
const catPos = type === 'bar' ? 'l' : 'b';
|
|
500
|
+
const valPos = type === 'bar' ? 'b' : 'l';
|
|
501
|
+
const between = type === 'area' ? 'midCat' : 'between';
|
|
502
|
+
const scaling = '<c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="0"/>';
|
|
503
|
+
const ticks = '<c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/>';
|
|
504
|
+
return `<c:catAx><c:axId val="10"/>${scaling}<c:axPos val="${catPos}"/>${ticks}<c:crossAx val="20"/><c:crosses val="autoZero"/><c:auto val="1"/><c:lblAlgn val="ctr"/><c:lblOffset val="100"/><c:noMultiLvlLbl val="0"/></c:catAx><c:valAx><c:axId val="20"/>${scaling}<c:axPos val="${valPos}"/><c:majorGridlines/>${ticks}<c:crossAx val="10"/><c:crosses val="autoZero"/><c:crossBetween val="${between}"/></c:valAx>`;
|
|
505
|
+
}
|
|
506
|
+
function skeleton(type, stacked, legend) {
|
|
507
|
+
const legendXml = legend === false
|
|
508
|
+
? ''
|
|
509
|
+
: `<c:legend><c:legendPos val="${LEGEND_POSITIONS[legend]}"/><c:overlay val="0"/></c:legend>`;
|
|
510
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
511
|
+
<c:chartSpace xmlns:c="${C}" xmlns:a="${A}" xmlns:r="${R}"><c:date1904 val="0"/><c:roundedCorners val="0"/><c:chart><c:autoTitleDeleted val="1"/><c:plotArea><c:layout/>${plotXml(type, stacked)}${axesXml(type)}</c:plotArea>${legendXml}<c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/></c:chart></c:chartSpace>`;
|
|
512
|
+
}
|
|
513
|
+
export function createChart(ref, spec, opts = {}) {
|
|
514
|
+
if (!isRecordLike(spec) || !isChartType(spec.type)) {
|
|
515
|
+
const type = isRecordLike(spec) ? spec.type : undefined;
|
|
516
|
+
throw new DocsError('UNSUPPORTED_CHART', `chart type must be one of ${CREATABLE.join(', ')}, got ${JSON.stringify(type)}`);
|
|
517
|
+
}
|
|
518
|
+
const type = spec.type;
|
|
519
|
+
const data = validateData(spec);
|
|
520
|
+
const stacked = spec.stacked ?? false;
|
|
521
|
+
if (stacked !== false && stacked !== true && stacked !== 'percent') {
|
|
522
|
+
throw new DocsError('INVALID_CHART', 'stacked must be false, true or "percent"');
|
|
523
|
+
}
|
|
524
|
+
const legend = spec.legend ?? 'right';
|
|
525
|
+
if (!isLegend(legend)) {
|
|
526
|
+
throw new DocsError('INVALID_CHART', 'legend must be false, "right", "left", "top" or "bottom"');
|
|
527
|
+
}
|
|
528
|
+
const doc = xml.parse(skeleton(type, stacked, legend));
|
|
529
|
+
const group = groupsOf(doc)[0];
|
|
530
|
+
for (let i = 0; i < data.series.length; i++) {
|
|
531
|
+
insertSeries(group, newSeries(doc, group));
|
|
532
|
+
}
|
|
533
|
+
ref.pkg.add(ref.part, doc, CHART_TYPE);
|
|
534
|
+
setChartData(ref, data, opts);
|
|
535
|
+
if (typeof spec.title === 'string' && spec.title) {
|
|
536
|
+
setChartTitle(ref, spec.title);
|
|
537
|
+
}
|
|
538
|
+
return ref.part;
|
|
539
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Element } from '../xml.ts';
|
|
2
|
+
export interface Paragraph {
|
|
3
|
+
text: string;
|
|
4
|
+
level?: number;
|
|
5
|
+
}
|
|
6
|
+
export type TextInput = string | (string | Paragraph)[];
|
|
7
|
+
export type Alignment = 'left' | 'center' | 'right' | 'justify';
|
|
8
|
+
export interface Style {
|
|
9
|
+
size?: number;
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
italic?: boolean;
|
|
12
|
+
underline?: boolean;
|
|
13
|
+
color?: string;
|
|
14
|
+
font?: string;
|
|
15
|
+
align?: Alignment;
|
|
16
|
+
}
|
|
17
|
+
export interface ReadParagraph {
|
|
18
|
+
text: string;
|
|
19
|
+
level: number;
|
|
20
|
+
}
|
|
21
|
+
export interface Losses {
|
|
22
|
+
hyperlinks?: number;
|
|
23
|
+
fields?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface StyleTarget {
|
|
26
|
+
paragraph?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function readParagraphs(txBody: Element): ReadParagraph[];
|
|
29
|
+
export declare function readText(txBody: Element): string;
|
|
30
|
+
export declare function normalizeParagraphs(input: unknown): Paragraph[];
|
|
31
|
+
export declare function losses(txBody: Element): Losses;
|
|
32
|
+
export declare function writeParagraphs(txBody: Element, input: unknown): number;
|
|
33
|
+
export declare function ensureTextBody(parent: Element, order?: readonly string[]): Element;
|
|
34
|
+
export declare function validateStyle(style: unknown): Style;
|
|
35
|
+
export declare function applyStyle(txBody: Element, input: unknown, target?: StyleTarget): number;
|