@qvac/harness 0.7.2 → 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 +70 -1
- package/NOTICE.md +16 -0
- package/dist-lib/lib/capabilities/completion.d.ts +1 -0
- package/dist-lib/lib/capabilities/completion.js +46 -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/dist-lib/lib/tools/weather-lookup/geocode.d.ts +26 -0
- package/dist-lib/lib/tools/weather-lookup/geocode.js +110 -0
- package/dist-lib/lib/tools/weather-lookup/place.js +5 -1
- package/dist-lib/lib/tools/weather-lookup.d.ts +2 -1
- package/dist-lib/lib/tools/weather-lookup.js +115 -84
- package/package.json +29 -1
- package/worker/harness-sidecar.mjs +3 -1
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
import { Package } from '../opc/package.js';
|
|
2
|
+
import * as xml from '../xml.js';
|
|
3
|
+
import * as media from '../opc/media.js';
|
|
4
|
+
import { probe } from '../image.js';
|
|
5
|
+
import * as refs from './refs.js';
|
|
6
|
+
import * as formula from './formula.js';
|
|
7
|
+
import { Sheet, SHEET_ORDER } from './sheet.js';
|
|
8
|
+
import { SharedStrings } from './strings.js';
|
|
9
|
+
import { Styles, validate as validateStyle } from './styles.js';
|
|
10
|
+
import * as properties from '../opc/properties.js';
|
|
11
|
+
import * as chart from '../drawingml/chart.js';
|
|
12
|
+
import * as reshape from './reshape.js';
|
|
13
|
+
import { build } from './template.js';
|
|
14
|
+
import { DocsError } from '../errors.js';
|
|
15
|
+
import { isRecord } from '../../../guards.js';
|
|
16
|
+
const { x: X, r: R, xdr: XDR, a: A } = xml.ns;
|
|
17
|
+
const STRICT_REL = 'http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument';
|
|
18
|
+
const REL_BASE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
|
|
19
|
+
const REL = {
|
|
20
|
+
officeDocument: REL_BASE + 'officeDocument',
|
|
21
|
+
worksheet: REL_BASE + 'worksheet',
|
|
22
|
+
calcChain: REL_BASE + 'calcChain',
|
|
23
|
+
drawing: REL_BASE + 'drawing',
|
|
24
|
+
chart: REL_BASE + 'chart',
|
|
25
|
+
vbaProject: 'http://schemas.microsoft.com/office/2006/relationships/vbaProject'
|
|
26
|
+
};
|
|
27
|
+
const SML = 'application/vnd.openxmlformats-officedocument.spreadsheetml.';
|
|
28
|
+
const TYPES = {
|
|
29
|
+
main: SML + 'sheet.main+xml',
|
|
30
|
+
worksheet: SML + 'worksheet+xml',
|
|
31
|
+
drawing: 'application/vnd.openxmlformats-officedocument.drawing+xml',
|
|
32
|
+
chart: 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml'
|
|
33
|
+
};
|
|
34
|
+
const MACRO_TYPES = new Set([
|
|
35
|
+
'application/vnd.ms-excel.sheet.macroEnabled.main+xml',
|
|
36
|
+
'application/vnd.ms-excel.template.macroEnabled.main+xml'
|
|
37
|
+
]);
|
|
38
|
+
const MAIN_TYPES = new Set([TYPES.main, SML + 'template.main+xml', ...MACRO_TYPES]);
|
|
39
|
+
const WORKBOOK_ORDER = [
|
|
40
|
+
'fileVersion',
|
|
41
|
+
'fileSharing',
|
|
42
|
+
'workbookPr',
|
|
43
|
+
'workbookProtection',
|
|
44
|
+
'bookViews',
|
|
45
|
+
'sheets',
|
|
46
|
+
'functionGroups',
|
|
47
|
+
'externalReferences',
|
|
48
|
+
'definedNames',
|
|
49
|
+
'calcPr',
|
|
50
|
+
'oleSize',
|
|
51
|
+
'customWorkbookViews',
|
|
52
|
+
'pivotCaches',
|
|
53
|
+
'smartTagPr',
|
|
54
|
+
'smartTagTypes',
|
|
55
|
+
'webPublishing',
|
|
56
|
+
'fileRecoveryPr',
|
|
57
|
+
'webPublishObjects',
|
|
58
|
+
'extLst'
|
|
59
|
+
];
|
|
60
|
+
const EMU_PER_PIXEL = 9525;
|
|
61
|
+
function validateRows(rows) {
|
|
62
|
+
if (!Array.isArray(rows) || rows.length === 0 || !rows.every((row) => Array.isArray(row))) {
|
|
63
|
+
throw new DocsError('INVALID_VALUE', 'rows must be a non-empty array of rows, each an array of values');
|
|
64
|
+
}
|
|
65
|
+
if (rows.length > 100000) {
|
|
66
|
+
throw new DocsError('INVALID_VALUE', 'add at most 100000 rows at a time');
|
|
67
|
+
}
|
|
68
|
+
return rows.filter((row) => Array.isArray(row));
|
|
69
|
+
}
|
|
70
|
+
function options(opts, name) {
|
|
71
|
+
if (!isRecord(opts) || Array.isArray(opts)) {
|
|
72
|
+
throw new DocsError('INVALID_OPTIONS', `${name} takes an options object`);
|
|
73
|
+
}
|
|
74
|
+
return opts;
|
|
75
|
+
}
|
|
76
|
+
function positiveCount(value) {
|
|
77
|
+
const count = value === undefined ? 1 : value;
|
|
78
|
+
if (!(typeof count === 'number' && Number.isInteger(count) && count >= 1)) {
|
|
79
|
+
throw new DocsError('INVALID_VALUE', 'count must be at least 1');
|
|
80
|
+
}
|
|
81
|
+
return count;
|
|
82
|
+
}
|
|
83
|
+
function emu(opts, key) {
|
|
84
|
+
const value = opts[key];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
if (!(typeof value === 'number' && Number.isInteger(value) && value > 0)) {
|
|
89
|
+
throw new DocsError('INVALID_BOX', `${key} must be a positive whole number of EMU, got ${String(value)}`);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function imageSize(opts, native) {
|
|
94
|
+
const width = emu(opts, 'width');
|
|
95
|
+
const height = emu(opts, 'height');
|
|
96
|
+
const ratio = native.height / native.width;
|
|
97
|
+
if (width === undefined && height === undefined) {
|
|
98
|
+
return { width: native.width * EMU_PER_PIXEL, height: native.height * EMU_PER_PIXEL };
|
|
99
|
+
}
|
|
100
|
+
if (width !== undefined && height !== undefined) {
|
|
101
|
+
return { width, height };
|
|
102
|
+
}
|
|
103
|
+
if (width !== undefined) {
|
|
104
|
+
return { width, height: Math.round(width * ratio) };
|
|
105
|
+
}
|
|
106
|
+
return { width: Math.round((height ?? 0) / ratio), height: height ?? 0 };
|
|
107
|
+
}
|
|
108
|
+
function categoryValue(v) {
|
|
109
|
+
if (typeof v === 'number')
|
|
110
|
+
return v;
|
|
111
|
+
return v === null ? '' : String(v);
|
|
112
|
+
}
|
|
113
|
+
function sheetName(raw) {
|
|
114
|
+
return raw.startsWith("'") && raw.endsWith("'") ? raw.slice(1, -1).replace(/''/g, "'") : raw;
|
|
115
|
+
}
|
|
116
|
+
function anchorStart(doc, anchor) {
|
|
117
|
+
const d = xml.builder(doc, XDR);
|
|
118
|
+
const text = (local, value) => {
|
|
119
|
+
const el = d(local);
|
|
120
|
+
xml.setText(el, String(value));
|
|
121
|
+
return el;
|
|
122
|
+
};
|
|
123
|
+
const root = d('oneCellAnchor');
|
|
124
|
+
const from = xml.append(root, d('from'));
|
|
125
|
+
from.appendChild(text('col', anchor.at.col));
|
|
126
|
+
from.appendChild(text('colOff', 0));
|
|
127
|
+
from.appendChild(text('row', anchor.at.row));
|
|
128
|
+
from.appendChild(text('rowOff', 0));
|
|
129
|
+
root.appendChild(d('ext', { cx: anchor.size.width, cy: anchor.size.height }));
|
|
130
|
+
return root;
|
|
131
|
+
}
|
|
132
|
+
function xfrmOf(parent, spec) {
|
|
133
|
+
const doc = xml.owner(parent);
|
|
134
|
+
const a = xml.builder(doc, A);
|
|
135
|
+
const size = spec.size;
|
|
136
|
+
const xfrm = xml.append(parent, xml.builder(doc, spec.namespace)('xfrm'));
|
|
137
|
+
xfrm.appendChild(a('off', { x: 0, y: 0 }));
|
|
138
|
+
xfrm.appendChild(a('ext', { cx: size.width, cy: size.height }));
|
|
139
|
+
}
|
|
140
|
+
function chartAnchor(doc, anchor, spec) {
|
|
141
|
+
const d = xml.builder(doc, XDR);
|
|
142
|
+
const a = xml.builder(doc, A);
|
|
143
|
+
const root = anchorStart(doc, anchor);
|
|
144
|
+
const frame = xml.append(root, d('graphicFrame', { macro: '' }));
|
|
145
|
+
const nv = xml.append(frame, d('nvGraphicFramePr'));
|
|
146
|
+
nv.appendChild(d('cNvPr', { id: spec.id, name: spec.name }));
|
|
147
|
+
nv.appendChild(d('cNvGraphicFramePr'));
|
|
148
|
+
xfrmOf(frame, { namespace: XDR, size: { width: 0, height: 0 } });
|
|
149
|
+
const data = xml.append(xml.append(frame, a('graphic')), a('graphicData', { uri: chart.GRAPHIC_URI }));
|
|
150
|
+
const ref = xml.append(data, xml.builder(doc, chart.C)('chart'));
|
|
151
|
+
xml.setNsAttribute(ref, { namespace: R, local: 'id' }, spec.rId);
|
|
152
|
+
root.appendChild(d('clientData'));
|
|
153
|
+
return root;
|
|
154
|
+
}
|
|
155
|
+
function pictureAnchor(doc, anchor, spec) {
|
|
156
|
+
const d = xml.builder(doc, XDR);
|
|
157
|
+
const a = xml.builder(doc, A);
|
|
158
|
+
const root = anchorStart(doc, anchor);
|
|
159
|
+
const pic = xml.append(root, d('pic'));
|
|
160
|
+
const nv = xml.append(pic, d('nvPicPr'));
|
|
161
|
+
nv.appendChild(d('cNvPr', { id: spec.id, name: spec.name, descr: spec.description }));
|
|
162
|
+
xml.append(nv, d('cNvPicPr')).appendChild(a('picLocks', { noChangeAspect: 1 }));
|
|
163
|
+
const fill = xml.append(pic, d('blipFill'));
|
|
164
|
+
const blip = xml.append(fill, a('blip'));
|
|
165
|
+
xml.setNsAttribute(blip, { namespace: R, local: 'embed' }, spec.rId);
|
|
166
|
+
xml.append(fill, a('stretch')).appendChild(a('fillRect'));
|
|
167
|
+
const spPr = xml.append(pic, d('spPr'));
|
|
168
|
+
xfrmOf(spPr, { namespace: A, size: spec.size });
|
|
169
|
+
xml.append(spPr, a('prstGeom', { prst: 'rect' })).appendChild(a('avLst'));
|
|
170
|
+
root.appendChild(d('clientData'));
|
|
171
|
+
return root;
|
|
172
|
+
}
|
|
173
|
+
function nextDrawingId(doc) {
|
|
174
|
+
return (Math.max(0, ...xml.descendants(doc, XDR, 'cNvPr').map((el) => Number(el.getAttribute('id')) || 0)) + 1);
|
|
175
|
+
}
|
|
176
|
+
export class Workbook {
|
|
177
|
+
package;
|
|
178
|
+
main;
|
|
179
|
+
strings;
|
|
180
|
+
styles;
|
|
181
|
+
state;
|
|
182
|
+
sheetsChanged;
|
|
183
|
+
constructor(pkg) {
|
|
184
|
+
this.package = pkg;
|
|
185
|
+
const main = pkg.related('/', REL.officeDocument)[0];
|
|
186
|
+
if (!main || !MAIN_TYPES.has(pkg.contentType(main) ?? '')) {
|
|
187
|
+
if (pkg.rels('/').byType(STRICT_REL).length) {
|
|
188
|
+
throw new DocsError('UNSUPPORTED_XLSX', 'the workbook uses Strict Open XML, which is not supported');
|
|
189
|
+
}
|
|
190
|
+
throw new DocsError('NOT_XLSX', 'the file is not an Excel workbook');
|
|
191
|
+
}
|
|
192
|
+
this.main = main;
|
|
193
|
+
const root = pkg.root(main);
|
|
194
|
+
if (root.namespaceURI !== X || root.localName !== 'workbook') {
|
|
195
|
+
throw new DocsError('UNSUPPORTED_XLSX', 'the workbook uses Strict Open XML, which is not supported');
|
|
196
|
+
}
|
|
197
|
+
this.strings = new SharedStrings(pkg, main);
|
|
198
|
+
this.styles = new Styles(pkg, main);
|
|
199
|
+
this.state = { touched: new Set(), formulas: false, cellsChanged: false };
|
|
200
|
+
this.sheetsChanged = false;
|
|
201
|
+
}
|
|
202
|
+
static open(bytes, opts) {
|
|
203
|
+
return new Workbook(Package.open(bytes, opts));
|
|
204
|
+
}
|
|
205
|
+
static create(opts = {}) {
|
|
206
|
+
const pkg = Package.create({ strict: opts.strict });
|
|
207
|
+
build(pkg, opts);
|
|
208
|
+
return new Workbook(pkg);
|
|
209
|
+
}
|
|
210
|
+
get changed() {
|
|
211
|
+
return this.package.changed;
|
|
212
|
+
}
|
|
213
|
+
get date1904() {
|
|
214
|
+
const value = xml.child(this.root(), X, 'workbookPr')?.getAttribute('date1904');
|
|
215
|
+
return value === '1' || value === 'true';
|
|
216
|
+
}
|
|
217
|
+
get hasMacros() {
|
|
218
|
+
return MACRO_TYPES.has(this.package.contentType(this.main) ?? '');
|
|
219
|
+
}
|
|
220
|
+
sheets() {
|
|
221
|
+
return this.entries().map((entry) => {
|
|
222
|
+
const out = {
|
|
223
|
+
index: entry.index,
|
|
224
|
+
name: entry.name,
|
|
225
|
+
kind: entry.kind,
|
|
226
|
+
state: entry.state
|
|
227
|
+
};
|
|
228
|
+
if (entry.kind === 'worksheet') {
|
|
229
|
+
const used = new Sheet(this, entry).usedRange();
|
|
230
|
+
out.range = used ? refs.range(used) : null;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
read(sheet, opts) {
|
|
236
|
+
const target = this.sheet(sheet);
|
|
237
|
+
const out = target.read(isRecord(opts) ? opts : {});
|
|
238
|
+
const charts = this.charts(target);
|
|
239
|
+
if (charts.length) {
|
|
240
|
+
out.charts = charts;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
get(sheet, ref) {
|
|
245
|
+
return this.sheet(sheet).get(ref);
|
|
246
|
+
}
|
|
247
|
+
setCells(sheet, cells) {
|
|
248
|
+
const target = this.sheet(sheet);
|
|
249
|
+
if (!isRecord(cells) || Array.isArray(cells) || !Object.keys(cells).length) {
|
|
250
|
+
throw new DocsError('INVALID_VALUE', 'cells must be an object like { "A1": "Name", "B2": 42 }');
|
|
251
|
+
}
|
|
252
|
+
for (const ref of Object.keys(cells)) {
|
|
253
|
+
refs.parse(ref);
|
|
254
|
+
}
|
|
255
|
+
for (const [ref, value] of Object.entries(cells)) {
|
|
256
|
+
target.write(ref, value);
|
|
257
|
+
}
|
|
258
|
+
return { cells: Object.keys(cells).length };
|
|
259
|
+
}
|
|
260
|
+
format(sheet, range, style) {
|
|
261
|
+
const target = this.sheet(sheet);
|
|
262
|
+
const area = refs.parseRange(range);
|
|
263
|
+
const count = (area.bottom - area.top + 1) * (area.right - area.left + 1);
|
|
264
|
+
if (count > 100000) {
|
|
265
|
+
throw new DocsError('INVALID_RANGE', `format at most 100000 cells at a time, got ${count}`);
|
|
266
|
+
}
|
|
267
|
+
if (!isRecord(style) || Array.isArray(style) || !Object.keys(style).length) {
|
|
268
|
+
throw new DocsError('INVALID_STYLE', 'style must name something to change, like { bold: true }');
|
|
269
|
+
}
|
|
270
|
+
validateStyle(style);
|
|
271
|
+
for (let row = area.top; row <= area.bottom; row++) {
|
|
272
|
+
for (let col = area.left; col <= area.right; col++) {
|
|
273
|
+
target.write(refs.cell(row, col), style);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { cells: count };
|
|
277
|
+
}
|
|
278
|
+
appendRows(sheet, rows) {
|
|
279
|
+
const target = this.sheet(sheet);
|
|
280
|
+
const list = validateRows(rows);
|
|
281
|
+
const start = target.lastRow() + 1;
|
|
282
|
+
this.writeRows(target, { start, rows: list });
|
|
283
|
+
return { from: start + 1, to: start + list.length };
|
|
284
|
+
}
|
|
285
|
+
insertRows(sheet, opts) {
|
|
286
|
+
const target = this.sheet(sheet);
|
|
287
|
+
const o = options(opts, 'insertRows');
|
|
288
|
+
const row = refs.rowArg(o.at, 'at');
|
|
289
|
+
if ((o.rows === undefined) === (o.count === undefined)) {
|
|
290
|
+
throw new DocsError('INVALID_VALUE', 'give the rows to insert, or how many empty rows');
|
|
291
|
+
}
|
|
292
|
+
const list = o.rows === undefined ? null : validateRows(o.rows);
|
|
293
|
+
const count = list ? list.length : o.count;
|
|
294
|
+
if (!(typeof count === 'number' && Number.isInteger(count) && count >= 1)) {
|
|
295
|
+
throw new DocsError('INVALID_VALUE', 'give the rows to insert, or how many empty rows');
|
|
296
|
+
}
|
|
297
|
+
const change = { sheet: target.name, axis: 'row', at: row, count, kind: 'insert' };
|
|
298
|
+
this.reshapeWith(target, { change, action: 'insert rows' }, () => target.shiftRows(row, count));
|
|
299
|
+
if (list) {
|
|
300
|
+
this.writeRows(target, { start: row, rows: list, styleFrom: row + count });
|
|
301
|
+
}
|
|
302
|
+
return { from: row + 1, to: row + count };
|
|
303
|
+
}
|
|
304
|
+
deleteRows(sheet, opts) {
|
|
305
|
+
const target = this.sheet(sheet);
|
|
306
|
+
const o = options(opts, 'deleteRows');
|
|
307
|
+
const row = refs.rowArg(o.at, 'at');
|
|
308
|
+
const count = positiveCount(o.count);
|
|
309
|
+
const change = { sheet: target.name, axis: 'row', at: row, count, kind: 'delete' };
|
|
310
|
+
this.reshapeWith(target, { change, action: 'delete rows' }, () => target.removeRows(row, count));
|
|
311
|
+
return { deleted: count };
|
|
312
|
+
}
|
|
313
|
+
deleteColumns(sheet, opts) {
|
|
314
|
+
const target = this.sheet(sheet);
|
|
315
|
+
const o = options(opts, 'deleteColumns');
|
|
316
|
+
const col = refs.columnArg(o.column);
|
|
317
|
+
const count = positiveCount(o.count);
|
|
318
|
+
const change = { sheet: target.name, axis: 'col', at: col, count, kind: 'delete' };
|
|
319
|
+
this.reshapeWith(target, { change, action: 'delete columns' }, () => target.removeColumns(col, count));
|
|
320
|
+
return { deleted: count };
|
|
321
|
+
}
|
|
322
|
+
setColumnWidth(sheet, opts) {
|
|
323
|
+
const target = this.sheet(sheet);
|
|
324
|
+
const o = options(opts, 'setColumnWidth');
|
|
325
|
+
const col = refs.columnArg(o.column);
|
|
326
|
+
const width = o.width;
|
|
327
|
+
if (!(typeof width === 'number' && width > 0 && width <= 255)) {
|
|
328
|
+
throw new DocsError('INVALID_VALUE', `width is in characters, from 0 to 255, got ${String(width)}`);
|
|
329
|
+
}
|
|
330
|
+
target.setColumnWidth({ col, width });
|
|
331
|
+
return { changed: true };
|
|
332
|
+
}
|
|
333
|
+
addSheet(name, opts = {}) {
|
|
334
|
+
const valid = this.validateName(name, null);
|
|
335
|
+
const entries = this.entries();
|
|
336
|
+
const at = isRecord(opts) && opts.at !== undefined ? opts.at : entries.length;
|
|
337
|
+
if (!(typeof at === 'number' && Number.isInteger(at) && at >= 0 && at <= entries.length)) {
|
|
338
|
+
throw new DocsError('SHEET_NOT_FOUND', `at must be a position from 0 to ${entries.length}, got ${String(at)}`);
|
|
339
|
+
}
|
|
340
|
+
const pkg = this.package;
|
|
341
|
+
const part = pkg.freeName('/xl/worksheets/sheet%d.xml');
|
|
342
|
+
pkg.add(part, xml.parse(`<worksheet xmlns="${X}" xmlns:r="${R}"><sheetData/></worksheet>`), TYPES.worksheet);
|
|
343
|
+
const rId = pkg.rels(this.main).add(REL.worksheet, part);
|
|
344
|
+
const sheetId = Math.max(0, ...entries.map((e) => Number(e.el.getAttribute('sheetId')))) + 1;
|
|
345
|
+
const el = xml.setAttributes(xml.named(this.doc(), 'sheet', X), { name: valid, sheetId });
|
|
346
|
+
xml.setNsAttribute(el, { namespace: R, local: 'id' }, rId);
|
|
347
|
+
this.sheetList().insertBefore(el, entries[at]?.el ?? null);
|
|
348
|
+
this.shiftSheetIndexes(at, 1);
|
|
349
|
+
pkg.touch(this.main);
|
|
350
|
+
this.sheetsChanged = true;
|
|
351
|
+
return { index: at, name: valid };
|
|
352
|
+
}
|
|
353
|
+
renameSheet(sheet, name) {
|
|
354
|
+
const entry = this.entry(sheet);
|
|
355
|
+
if (entry.name === name) {
|
|
356
|
+
return { changed: false };
|
|
357
|
+
}
|
|
358
|
+
const to = this.validateName(name, entry);
|
|
359
|
+
const from = entry.name;
|
|
360
|
+
entry.el.setAttribute('name', to);
|
|
361
|
+
this.package.touch(this.main);
|
|
362
|
+
this.sheetsChanged = true;
|
|
363
|
+
return { changed: true, formulas: this.renameReferences(from, to) };
|
|
364
|
+
}
|
|
365
|
+
renameReferences(from, to) {
|
|
366
|
+
let updated = 0;
|
|
367
|
+
const rewrite = (el) => {
|
|
368
|
+
const next = formula.renameSheet(xml.text(el), { from, to });
|
|
369
|
+
if (next === xml.text(el)) {
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
xml.setText(el, next);
|
|
373
|
+
updated++;
|
|
374
|
+
return true;
|
|
375
|
+
};
|
|
376
|
+
for (const other of this.entries().filter((e) => e.kind === 'worksheet')) {
|
|
377
|
+
if (this.rewriteAll(reshape.formulaElements(this.package.xml(other.part)), rewrite)) {
|
|
378
|
+
this.package.touch(other.part);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const dn of this.definedNames()) {
|
|
382
|
+
rewrite(dn);
|
|
383
|
+
}
|
|
384
|
+
for (const part of this.chartParts()) {
|
|
385
|
+
if (this.rewriteAll(xml.descendants(this.package.xml(part), xml.ns.c, 'f'), rewrite)) {
|
|
386
|
+
this.package.touch(part);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
reshape.renamePivotSources(this.package, from, to);
|
|
390
|
+
return updated;
|
|
391
|
+
}
|
|
392
|
+
deleteSheet(sheet) {
|
|
393
|
+
const entry = this.entry(sheet);
|
|
394
|
+
const visible = this.entries().filter((e) => e.state === 'visible' && e.part !== entry.part);
|
|
395
|
+
if (visible.length === 0) {
|
|
396
|
+
throw new DocsError('LAST_SHEET', `"${entry.name}" is the only visible sheet; a workbook needs at least one`);
|
|
397
|
+
}
|
|
398
|
+
const users = this.references(entry, { includeOwn: false });
|
|
399
|
+
if (users.length) {
|
|
400
|
+
throw new DocsError('SHEET_IS_REFERENCED', `"${entry.name}" is used by ${users.join(', ')}; those would turn into #REF! errors, so change them first`);
|
|
401
|
+
}
|
|
402
|
+
for (const dn of this.definedNames()) {
|
|
403
|
+
const local = dn.getAttribute('localSheetId');
|
|
404
|
+
if (local !== null && local !== '' && Number(local) === entry.index) {
|
|
405
|
+
xml.remove(dn);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this.shiftSheetIndexes(entry.index + 1, -1);
|
|
409
|
+
xml.remove(entry.el);
|
|
410
|
+
this.package.rels(this.main).remove(entry.rId);
|
|
411
|
+
this.package.remove(entry.part);
|
|
412
|
+
const names = xml.child(this.root(), X, 'definedNames');
|
|
413
|
+
if (names && !xml.children(names, X, 'definedName').length) {
|
|
414
|
+
xml.remove(names);
|
|
415
|
+
}
|
|
416
|
+
this.package.touch(this.main);
|
|
417
|
+
this.sheetsChanged = true;
|
|
418
|
+
return { sheets: this.entries().length };
|
|
419
|
+
}
|
|
420
|
+
addImage(sheet, bytes, opts = {}) {
|
|
421
|
+
const target = this.sheet(sheet);
|
|
422
|
+
const info = probe(bytes);
|
|
423
|
+
const o = isRecord(opts) ? opts : {};
|
|
424
|
+
const at = refs.parse(o.cell === undefined ? 'A1' : o.cell);
|
|
425
|
+
const size = imageSize(o, info);
|
|
426
|
+
const drawing = this.ensureDrawing(target);
|
|
427
|
+
const { rId } = media.addImage(this.package, bytes, { source: drawing, dir: '../media' });
|
|
428
|
+
const doc = this.package.xml(drawing);
|
|
429
|
+
const id = nextDrawingId(doc);
|
|
430
|
+
const name = typeof o.name === 'string' && o.name ? o.name : `Picture ${id}`;
|
|
431
|
+
const description = typeof o.description === 'string' ? o.description : undefined;
|
|
432
|
+
xml
|
|
433
|
+
.root(doc)
|
|
434
|
+
.appendChild(pictureAnchor(doc, { at, size }, { id, rId, name, description, size }));
|
|
435
|
+
this.package.touch(drawing);
|
|
436
|
+
return { cell: refs.cell(at.row, at.col), ...size };
|
|
437
|
+
}
|
|
438
|
+
addChart(sheet, input) {
|
|
439
|
+
const target = this.sheet(sheet);
|
|
440
|
+
const spec = isRecord(input) ? input : {};
|
|
441
|
+
if (typeof spec.categories !== 'string') {
|
|
442
|
+
throw new DocsError('INVALID_CHART_DATA', 'categories must be a range like "A2:A5"');
|
|
443
|
+
}
|
|
444
|
+
if (!Array.isArray(spec.series) || spec.series.length === 0) {
|
|
445
|
+
throw new DocsError('INVALID_CHART_DATA', 'series must be a non-empty array of { values: "B2:B5", name }');
|
|
446
|
+
}
|
|
447
|
+
const categories = this.rangeValues(target, spec.categories);
|
|
448
|
+
const series = spec.series.map((s, i) => this.planSeries(target, { s, i, count: categories.values.length }));
|
|
449
|
+
const at = spec.cell === undefined ? this.besideData(target) : refs.parse(spec.cell);
|
|
450
|
+
const width = emu(spec, 'width') ?? 5486400;
|
|
451
|
+
const height = emu(spec, 'height') ?? 3200400;
|
|
452
|
+
const pkg = this.package;
|
|
453
|
+
const part = pkg.freeName('/xl/charts/chart%d.xml');
|
|
454
|
+
chart.createChart({ pkg, part }, {
|
|
455
|
+
type: spec.type,
|
|
456
|
+
title: spec.title,
|
|
457
|
+
stacked: spec.stacked,
|
|
458
|
+
legend: spec.legend,
|
|
459
|
+
categories: categories.values.map(categoryValue),
|
|
460
|
+
series: series.map(({ name, values }) => ({ name, values }))
|
|
461
|
+
}, { links: { categories: categories.formula, series: series.map((s) => s.link) } });
|
|
462
|
+
this.placeChart(target, { part, anchor: { at, size: { width, height } }, name: spec.name });
|
|
463
|
+
return { cell: refs.cell(at.row, at.col), width, height };
|
|
464
|
+
}
|
|
465
|
+
removeMacros() {
|
|
466
|
+
if (!this.hasMacros) {
|
|
467
|
+
return { changed: false };
|
|
468
|
+
}
|
|
469
|
+
this.package.contentTypes.setOverride(this.main, TYPES.main);
|
|
470
|
+
for (const part of this.package.related(this.main, REL.vbaProject)) {
|
|
471
|
+
this.package.remove(part);
|
|
472
|
+
}
|
|
473
|
+
const rels = this.package.rels(this.main);
|
|
474
|
+
for (const rel of rels.byType(REL.vbaProject)) {
|
|
475
|
+
rels.remove(rel.id);
|
|
476
|
+
}
|
|
477
|
+
return { changed: true };
|
|
478
|
+
}
|
|
479
|
+
save(opts) {
|
|
480
|
+
for (const sheet of this.state.touched) {
|
|
481
|
+
if (this.package.has(sheet.part)) {
|
|
482
|
+
sheet.updateDimension();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (this.state.cellsChanged) {
|
|
486
|
+
this.dropCalcChain();
|
|
487
|
+
}
|
|
488
|
+
if (this.state.formulas) {
|
|
489
|
+
this.requestRecalc();
|
|
490
|
+
}
|
|
491
|
+
if (this.sheetsChanged) {
|
|
492
|
+
properties.refreshSummary(this.package);
|
|
493
|
+
this.sheetsChanged = false;
|
|
494
|
+
}
|
|
495
|
+
properties.applySaveOptions(this.package, opts);
|
|
496
|
+
this.state.touched.clear();
|
|
497
|
+
this.state.cellsChanged = false;
|
|
498
|
+
this.state.formulas = false;
|
|
499
|
+
return this.package.save();
|
|
500
|
+
}
|
|
501
|
+
host() {
|
|
502
|
+
return {
|
|
503
|
+
package: this.package,
|
|
504
|
+
main: this.main,
|
|
505
|
+
state: this.state,
|
|
506
|
+
entries: () => this.entries(),
|
|
507
|
+
definedNames: () => this.definedNames(),
|
|
508
|
+
chartParts: () => this.chartParts(),
|
|
509
|
+
drawingOf: (sheet) => this.drawingOf(sheet),
|
|
510
|
+
sheetFor: (entry) => new Sheet(this, entry)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
reshapeWith(target, step, move) {
|
|
514
|
+
const host = this.host();
|
|
515
|
+
const reasons = reshape.blockers(host, target, step.change);
|
|
516
|
+
if (reasons.length) {
|
|
517
|
+
const fallback = step.action === 'insert rows'
|
|
518
|
+
? 'append the rows at the end instead'
|
|
519
|
+
: 'clear the cells instead';
|
|
520
|
+
throw new DocsError('SHEET_HAS_DEPENDENTS', `cannot ${step.action} on "${target.name}": it has ${reasons.join(', ')}, which cannot be moved yet; ${fallback}`);
|
|
521
|
+
}
|
|
522
|
+
reshape.prepare(host, target);
|
|
523
|
+
move();
|
|
524
|
+
reshape.apply(host, target, step.change);
|
|
525
|
+
}
|
|
526
|
+
rewriteAll(els, rewrite) {
|
|
527
|
+
let touched = false;
|
|
528
|
+
for (const el of els) {
|
|
529
|
+
if (rewrite(el)) {
|
|
530
|
+
touched = true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return touched;
|
|
534
|
+
}
|
|
535
|
+
dropCalcChain() {
|
|
536
|
+
const rels = this.package.rels(this.main);
|
|
537
|
+
for (const rel of rels.byType(REL.calcChain)) {
|
|
538
|
+
if (rel.part && this.package.has(rel.part)) {
|
|
539
|
+
this.package.remove(rel.part);
|
|
540
|
+
}
|
|
541
|
+
rels.remove(rel.id);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
requestRecalc() {
|
|
545
|
+
const root = this.root();
|
|
546
|
+
const calc = xml.child(root, X, 'calcPr') ??
|
|
547
|
+
xml.insertOrdered(root, xml.named(this.doc(), 'calcPr', X), WORKBOOK_ORDER);
|
|
548
|
+
if (calc.getAttribute('fullCalcOnLoad') === '1') {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
calc.setAttribute('fullCalcOnLoad', '1');
|
|
552
|
+
this.package.touch(this.main);
|
|
553
|
+
}
|
|
554
|
+
planSeries(target, item) {
|
|
555
|
+
const { s, i, count } = item;
|
|
556
|
+
if (!isRecord(s) || typeof s.values !== 'string') {
|
|
557
|
+
throw new DocsError('INVALID_CHART_DATA', `series ${i} needs values as a range like "B2:B5"`);
|
|
558
|
+
}
|
|
559
|
+
const values = this.rangeValues(target, s.values);
|
|
560
|
+
if (values.values.length !== count) {
|
|
561
|
+
throw new DocsError('INVALID_CHART_DATA', `series ${i} covers ${values.values.length} cells but the categories cover ${count}`);
|
|
562
|
+
}
|
|
563
|
+
const { name, nameRef } = this.seriesName(target, s, i);
|
|
564
|
+
return {
|
|
565
|
+
name,
|
|
566
|
+
values: values.values.map((v) => (typeof v === 'number' ? v : null)),
|
|
567
|
+
link: { name: nameRef, values: values.formula }
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
seriesName(target, s, i) {
|
|
571
|
+
let name = s.name;
|
|
572
|
+
let nameRef = null;
|
|
573
|
+
if (s.nameCell !== undefined) {
|
|
574
|
+
const cell = this.rangeValues(target, s.nameCell);
|
|
575
|
+
if (cell.values.length !== 1) {
|
|
576
|
+
throw new DocsError('INVALID_CHART_DATA', `nameCell of series ${i} must be one cell`);
|
|
577
|
+
}
|
|
578
|
+
nameRef = cell.formula;
|
|
579
|
+
if (name === undefined) {
|
|
580
|
+
name = cell.values[0] === null ? '' : String(cell.values[0]);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (name === undefined) {
|
|
584
|
+
name = `Series ${i + 1}`;
|
|
585
|
+
}
|
|
586
|
+
if (typeof name !== 'string') {
|
|
587
|
+
throw new DocsError('INVALID_CHART_DATA', `series ${i} name must be text`);
|
|
588
|
+
}
|
|
589
|
+
return { name, nameRef };
|
|
590
|
+
}
|
|
591
|
+
placeChart(target, place) {
|
|
592
|
+
const drawing = this.ensureDrawing(target);
|
|
593
|
+
const rId = this.package.rels(drawing).add(REL.chart, place.part);
|
|
594
|
+
const doc = this.package.xml(drawing);
|
|
595
|
+
const id = nextDrawingId(doc);
|
|
596
|
+
const name = typeof place.name === 'string' && place.name ? place.name : `Chart ${id}`;
|
|
597
|
+
xml.root(doc).appendChild(chartAnchor(doc, place.anchor, { id, rId, name }));
|
|
598
|
+
this.package.touch(drawing);
|
|
599
|
+
}
|
|
600
|
+
doc() {
|
|
601
|
+
return this.package.xml(this.main);
|
|
602
|
+
}
|
|
603
|
+
root() {
|
|
604
|
+
return xml.root(this.doc());
|
|
605
|
+
}
|
|
606
|
+
sheetList() {
|
|
607
|
+
const list = xml.child(this.root(), X, 'sheets');
|
|
608
|
+
if (!list) {
|
|
609
|
+
throw new DocsError('BROKEN_XLSX', 'the workbook lists no sheets');
|
|
610
|
+
}
|
|
611
|
+
return list;
|
|
612
|
+
}
|
|
613
|
+
entries() {
|
|
614
|
+
const rels = this.package.rels(this.main);
|
|
615
|
+
return xml.children(this.sheetList(), X, 'sheet').map((el, index) => {
|
|
616
|
+
const rId = el.getAttributeNS(R, 'id') ?? '';
|
|
617
|
+
const rel = rels.get(rId);
|
|
618
|
+
if (!rel || rel.external || !rel.part || !this.package.has(rel.part)) {
|
|
619
|
+
throw new DocsError('BROKEN_XLSX', `sheet "${xml.attr(el, 'name')}" points at a missing part`);
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
el,
|
|
623
|
+
index,
|
|
624
|
+
rId,
|
|
625
|
+
name: el.getAttribute('name') || '',
|
|
626
|
+
state: el.getAttribute('state') || 'visible',
|
|
627
|
+
part: rel.part,
|
|
628
|
+
kind: rel.type === REL.worksheet
|
|
629
|
+
? 'worksheet'
|
|
630
|
+
: (rel.type || 'unknown').split('/').pop() || 'unknown'
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
entry(ref) {
|
|
635
|
+
const entries = this.entries();
|
|
636
|
+
let found;
|
|
637
|
+
if (typeof ref === 'number') {
|
|
638
|
+
found = entries[ref];
|
|
639
|
+
}
|
|
640
|
+
else if (typeof ref === 'string') {
|
|
641
|
+
found = entries.find((e) => e.name.toLowerCase() === ref.toLowerCase());
|
|
642
|
+
}
|
|
643
|
+
if (!found) {
|
|
644
|
+
throw new DocsError('SHEET_NOT_FOUND', `no sheet ${JSON.stringify(ref)}; the workbook has ${entries.map((e) => `${e.index} "${e.name}"`).join(', ')}`);
|
|
645
|
+
}
|
|
646
|
+
return found;
|
|
647
|
+
}
|
|
648
|
+
sheet(ref) {
|
|
649
|
+
const entry = this.entry(ref);
|
|
650
|
+
if (entry.kind !== 'worksheet') {
|
|
651
|
+
throw new DocsError('NOT_WORKSHEET', `"${entry.name}" is a ${entry.kind}, not a worksheet`);
|
|
652
|
+
}
|
|
653
|
+
return new Sheet(this, entry);
|
|
654
|
+
}
|
|
655
|
+
validateName(name, self) {
|
|
656
|
+
if (typeof name !== 'string' || name.length < 1 || name.length > 31) {
|
|
657
|
+
throw new DocsError('INVALID_NAME', 'a sheet name is 1 to 31 characters');
|
|
658
|
+
}
|
|
659
|
+
if (/[:\\/?*[\]]/.test(name) || name.startsWith("'") || name.endsWith("'")) {
|
|
660
|
+
throw new DocsError('INVALID_NAME', 'a sheet name cannot contain : \\ / ? * [ ] or start or end with an apostrophe');
|
|
661
|
+
}
|
|
662
|
+
if (name.toLowerCase() === 'history') {
|
|
663
|
+
throw new DocsError('INVALID_NAME', '"History" is reserved by Excel');
|
|
664
|
+
}
|
|
665
|
+
const clash = this.entries().find((e) => (!self || e.part !== self.part) && e.name.toLowerCase() === name.toLowerCase());
|
|
666
|
+
if (clash) {
|
|
667
|
+
throw new DocsError('INVALID_NAME', `the workbook already has a sheet named "${clash.name}"`);
|
|
668
|
+
}
|
|
669
|
+
return name;
|
|
670
|
+
}
|
|
671
|
+
definedNames() {
|
|
672
|
+
const names = xml.child(this.root(), X, 'definedNames');
|
|
673
|
+
return names ? xml.children(names, X, 'definedName') : [];
|
|
674
|
+
}
|
|
675
|
+
chartParts() {
|
|
676
|
+
return this.package.names().filter((name) => this.package.contentType(name) === TYPES.chart);
|
|
677
|
+
}
|
|
678
|
+
formulaUsers(entry, query) {
|
|
679
|
+
const { includeOwn } = query;
|
|
680
|
+
const users = [];
|
|
681
|
+
for (const other of this.entries().filter((e) => e.kind === 'worksheet')) {
|
|
682
|
+
const texts = reshape.formulaElements(this.package.xml(other.part)).map((el) => xml.text(el));
|
|
683
|
+
if (other.part === entry.part) {
|
|
684
|
+
if (includeOwn && texts.length) {
|
|
685
|
+
users.push('formulas on this sheet');
|
|
686
|
+
}
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (texts.some((t) => formula.mentions(t, entry.name))) {
|
|
690
|
+
users.push(`formulas on "${other.name}"`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return users;
|
|
694
|
+
}
|
|
695
|
+
tableUsers(entry) {
|
|
696
|
+
const tables = reshape.tableNames(this.package, entry.part);
|
|
697
|
+
if (!tables.length) {
|
|
698
|
+
return [];
|
|
699
|
+
}
|
|
700
|
+
const uses = (text) => tables.some((table) => formula.usesTable(text, table));
|
|
701
|
+
const users = [];
|
|
702
|
+
for (const other of this.entries().filter((e) => e.kind === 'worksheet')) {
|
|
703
|
+
if (other.part === entry.part) {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (reshape.formulaElements(this.package.xml(other.part)).some((el) => uses(xml.text(el)))) {
|
|
707
|
+
users.push(`formulas on "${other.name}" that use table ${tables.join(', ')}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const names = this.definedNames().filter((dn) => uses(xml.text(dn)));
|
|
711
|
+
if (names.length) {
|
|
712
|
+
users.push(`defined names ${names.map((dn) => xml.attr(dn, 'name')).join(', ')}`);
|
|
713
|
+
}
|
|
714
|
+
return users;
|
|
715
|
+
}
|
|
716
|
+
references(entry, query) {
|
|
717
|
+
const { includeOwn } = query;
|
|
718
|
+
const users = this.formulaUsers(entry, query);
|
|
719
|
+
if (!includeOwn) {
|
|
720
|
+
users.push(...this.tableUsers(entry));
|
|
721
|
+
}
|
|
722
|
+
const names = this.definedNames().filter((dn) => {
|
|
723
|
+
const local = dn.getAttribute('localSheetId');
|
|
724
|
+
const own = local !== null && local !== '' && Number(local) === entry.index;
|
|
725
|
+
return (!own || includeOwn) && formula.mentions(xml.text(dn), entry.name);
|
|
726
|
+
});
|
|
727
|
+
if (names.length) {
|
|
728
|
+
users.push(`defined names ${names.map((dn) => xml.attr(dn, 'name')).join(', ')}`);
|
|
729
|
+
}
|
|
730
|
+
const own = includeOwn ? new Set() : new Set(this.ownCharts(entry));
|
|
731
|
+
const charts = this.chartParts().filter((part) => !own.has(part) &&
|
|
732
|
+
xml
|
|
733
|
+
.descendants(this.package.xml(part), xml.ns.c, 'f')
|
|
734
|
+
.some((f) => formula.mentions(xml.text(f), entry.name)));
|
|
735
|
+
if (charts.length) {
|
|
736
|
+
users.push(`${charts.length} chart(s)`);
|
|
737
|
+
}
|
|
738
|
+
return users;
|
|
739
|
+
}
|
|
740
|
+
writeRows(target, write) {
|
|
741
|
+
write.rows.forEach((row, i) => {
|
|
742
|
+
row.forEach((value, col) => {
|
|
743
|
+
if (value === null || value === undefined)
|
|
744
|
+
return;
|
|
745
|
+
const r = write.start + i;
|
|
746
|
+
target.write(refs.cell(r, col), value);
|
|
747
|
+
if (write.styleFrom === undefined) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const s = target.findCell({ row: write.styleFrom, col })?.getAttribute('s');
|
|
751
|
+
if (s) {
|
|
752
|
+
target.findCell({ row: r, col })?.setAttribute('s', s);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
shiftSheetIndexes(from, delta) {
|
|
758
|
+
for (const dn of this.definedNames()) {
|
|
759
|
+
const local = dn.getAttribute('localSheetId');
|
|
760
|
+
if (local !== null && local !== '' && Number(local) >= from) {
|
|
761
|
+
dn.setAttribute('localSheetId', String(Number(local) + delta));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const views = xml.child(this.root(), X, 'bookViews');
|
|
765
|
+
const count = this.entries().length + (delta < 0 ? -1 : 0);
|
|
766
|
+
for (const view of views ? xml.children(views, X, 'workbookView') : []) {
|
|
767
|
+
for (const attr of ['activeTab', 'firstSheet']) {
|
|
768
|
+
if (!view.hasAttribute(attr))
|
|
769
|
+
continue;
|
|
770
|
+
let n = Number(view.getAttribute(attr));
|
|
771
|
+
if (n >= from) {
|
|
772
|
+
n += delta;
|
|
773
|
+
}
|
|
774
|
+
view.setAttribute(attr, String(Math.max(0, Math.min(n, count - 1))));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
charts(sheet) {
|
|
779
|
+
const drawing = this.drawingOf(sheet);
|
|
780
|
+
if (!drawing) {
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
const rels = this.package.rels(drawing);
|
|
784
|
+
const out = [];
|
|
785
|
+
for (const anchor of xml.children(this.package.root(drawing), XDR)) {
|
|
786
|
+
const ref = xml.descendants(anchor, xml.ns.c, 'chart')[0];
|
|
787
|
+
const rel = ref ? rels.get(ref.getAttributeNS(R, 'id')) : null;
|
|
788
|
+
if (!rel || rel.external || !rel.part || !this.package.has(rel.part)) {
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
const from = xml.child(anchor, XDR, 'from');
|
|
792
|
+
const info = chart.readChart(this.package.xml(rel.part));
|
|
793
|
+
if (from) {
|
|
794
|
+
info.cell = refs.cell(Number(xml.text(xml.child(from, XDR, 'row'))), Number(xml.text(xml.child(from, XDR, 'col'))));
|
|
795
|
+
}
|
|
796
|
+
out.push(info);
|
|
797
|
+
}
|
|
798
|
+
return out;
|
|
799
|
+
}
|
|
800
|
+
ownCharts(entry) {
|
|
801
|
+
const drawing = this.drawingOf(new Sheet(this, entry));
|
|
802
|
+
return drawing ? this.package.related(drawing, REL.chart) : [];
|
|
803
|
+
}
|
|
804
|
+
rangeValues(defaultSheet, ref) {
|
|
805
|
+
if (typeof ref !== 'string') {
|
|
806
|
+
throw new DocsError('INVALID_RANGE', 'a range is text like A2:A5 or Sales!B2:B5');
|
|
807
|
+
}
|
|
808
|
+
const bang = ref.lastIndexOf('!');
|
|
809
|
+
const sheet = bang === -1 ? defaultSheet : this.sheet(sheetName(ref.slice(0, bang)));
|
|
810
|
+
const area = refs.parseRange((bang === -1 ? ref : ref.slice(bang + 1)).replace(/\$/g, ''));
|
|
811
|
+
if (area.top !== area.bottom && area.left !== area.right) {
|
|
812
|
+
throw new DocsError('INVALID_RANGE', `${ref} must be a single row or a single column`);
|
|
813
|
+
}
|
|
814
|
+
const byPos = new Map(sheet.cells().map(({ el, row, col }) => [`${row}:${col}`, el]));
|
|
815
|
+
const values = [];
|
|
816
|
+
for (let row = area.top; row <= area.bottom; row++) {
|
|
817
|
+
for (let col = area.left; col <= area.right; col++) {
|
|
818
|
+
const el = byPos.get(`${row}:${col}`);
|
|
819
|
+
values.push(el ? sheet.value(el) : null);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return { values, formula: refs.absolute(sheet.name, area) };
|
|
823
|
+
}
|
|
824
|
+
besideData(sheet) {
|
|
825
|
+
const used = sheet.usedRange();
|
|
826
|
+
return used ? { row: used.top, col: used.right + 2 } : { row: 0, col: 0 };
|
|
827
|
+
}
|
|
828
|
+
drawingOf(sheet) {
|
|
829
|
+
const ref = xml.child(sheet.root, X, 'drawing');
|
|
830
|
+
if (!ref) {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
const rel = this.package.rels(sheet.part).get(ref.getAttributeNS(R, 'id'));
|
|
834
|
+
return rel && !rel.external && rel.part && this.package.has(rel.part) ? rel.part : null;
|
|
835
|
+
}
|
|
836
|
+
ensureDrawing(sheet) {
|
|
837
|
+
const existing = this.drawingOf(sheet);
|
|
838
|
+
if (existing) {
|
|
839
|
+
return existing;
|
|
840
|
+
}
|
|
841
|
+
const part = this.package.freeName('/xl/drawings/drawing%d.xml');
|
|
842
|
+
this.package.add(part, xml.parse(`<xdr:wsDr xmlns:xdr="${XDR}" xmlns:a="${A}" xmlns:r="${R}"/>`), TYPES.drawing);
|
|
843
|
+
const rId = this.package.rels(sheet.part).add(REL.drawing, part);
|
|
844
|
+
const el = xml.named(sheet.doc, 'drawing', X);
|
|
845
|
+
xml.setNsAttribute(el, { namespace: R, local: 'id' }, rId);
|
|
846
|
+
xml.insertOrdered(sheet.root, el, SHEET_ORDER);
|
|
847
|
+
sheet.touch();
|
|
848
|
+
return part;
|
|
849
|
+
}
|
|
850
|
+
}
|