documonster 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
211
211
 
212
212
  ```html
213
213
  <!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
214
- <script src="https://unpkg.com/documonster@0.18.1/dist/iife/documonster.excel.iife.min.js"></script>
214
+ <script src="https://unpkg.com/documonster@0.18.2/dist/iife/documonster.excel.iife.min.js"></script>
215
215
  <script>
216
216
  const { Workbook, Cell } = Documonster.Excel;
217
217
  const wb = Workbook.create();
package/README_zh.md CHANGED
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
198
198
 
199
199
  ```html
200
200
  <!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
201
- <script src="https://unpkg.com/documonster@0.18.1/dist/iife/documonster.excel.iife.min.js"></script>
201
+ <script src="https://unpkg.com/documonster@0.18.2/dist/iife/documonster.excel.iife.min.js"></script>
202
202
  <script>
203
203
  const { Workbook, Cell } = Documonster.Excel;
204
204
  const wb = Workbook.create();
@@ -12,6 +12,7 @@
12
12
  * this exact sequence. An earlier version of this module's documentation claimed no such authority existed;
13
13
  * it does, in the same chapter as the filter example that was already being relied on.
14
14
  */
15
+ import { encodeWideString } from "./binary.js";
15
16
  import { BinaryWriter, concatUint8Arrays } from "../../../utils/binary.js";
16
17
  import { dateToExcel } from "#platform/utils/utils";
17
18
  /**
@@ -21,15 +22,6 @@ import { dateToExcel } from "#platform/utils/utils";
21
22
  */
22
23
  const CACHE_APP_VERSION = 8;
23
24
  const CACHE_MIN_VERSION = 3;
24
- /** An `XLWideString`: a four-byte character count, then UTF-16. */
25
- function wideString(value) {
26
- const characters = [...value];
27
- const writer = new BinaryWriter().writeUint32(characters.length);
28
- for (const character of characters.join("")) {
29
- writer.writeUint16(character.charCodeAt(0));
30
- }
31
- return writer.toUint8Array();
32
- }
33
25
  /** An `RfX`: four unsigned 32-bit bounds, first row, last row, first column, last column. */
34
26
  function rfx(rowFirst, rowLast, columnFirst, columnLast) {
35
27
  return new BinaryWriter()
@@ -66,7 +58,7 @@ export function pivotCacheIdRecords(caches) {
66
58
  "BrtBeginPivotCacheID",
67
59
  concatUint8Arrays([
68
60
  new BinaryWriter().writeUint32(cache.cacheId).toUint8Array(),
69
- wideString(cache.relationshipId)
61
+ encodeWideString(cache.relationshipId)
70
62
  ])
71
63
  ]);
72
64
  records.push(["BrtEndPivotCacheID", undefined]);
@@ -180,7 +172,7 @@ function cacheDefinitionPayload(cache) {
180
172
  .writeUint32(cache.records.length);
181
173
  const parts = [writer.toUint8Array()];
182
174
  if (hasRecords) {
183
- parts.push(wideString(cache.recordsRelationshipId));
175
+ parts.push(encodeWideString(cache.recordsRelationshipId));
184
176
  }
185
177
  // Present because `fLoadRefreshedWho` is 0 — see the note above.
186
178
  parts.push(new BinaryWriter().writeUint32(0).toUint8Array());
@@ -203,7 +195,7 @@ function sourceRangePayload(cache) {
203
195
  // single sheet, which is what makes `sheetName` meaningful.
204
196
  .writeUint8(0x02)
205
197
  .toUint8Array(),
206
- wideString(cache.sheetName),
198
+ encodeWideString(cache.sheetName),
207
199
  rfx(cache.range.rowFirst, cache.range.rowLast, cache.range.columnFirst, cache.range.columnLast)
208
200
  ]);
209
201
  }
@@ -228,7 +220,7 @@ function cacheFieldPayload(field) {
228
220
  // `cIsxtmps` — member-property field indices, which only an OLAP cache has.
229
221
  .writeUint32(0)
230
222
  .toUint8Array(),
231
- wideString(field.name)
223
+ encodeWideString(field.name)
232
224
  ]);
233
225
  }
234
226
  /**
@@ -393,7 +385,7 @@ function sharedItemRecord(item) {
393
385
  // A value none of the above: written as text. `String(item)` on a `Date` is what used to happen here, and
394
386
  // it put `"Mon Jan 15 2024 00:00:00 GMT+0800 (China Standard Time)"` into the cache — a sentence whose
395
387
  // content depends on the machine's locale and timezone.
396
- return ["BrtPCDIString", wideString(String(item))];
388
+ return ["BrtPCDIString", encodeWideString(String(item))];
397
389
  }
398
390
  /**
399
391
  * `PCDIDateTime`, MS-XLSB 2.5.101 — a calendar date and a wall-clock time, not a serial number.
@@ -470,7 +462,7 @@ function cacheRecordPayload(row, fields) {
470
462
  parts.push(encodePcdiDateTime(value));
471
463
  continue;
472
464
  }
473
- parts.push(wideString(value === null || value === undefined ? "" : String(value)));
465
+ parts.push(encodeWideString(value === null || value === undefined ? "" : String(value)));
474
466
  }
475
467
  return concatUint8Arrays(parts);
476
468
  }
@@ -16,6 +16,7 @@
16
16
  import { METRIC_DISPLAY_NAMES, VALID_SUBTOTALS } from "../core/pivot-table-types.js";
17
17
  import { decodeCell } from "../utils/address.js";
18
18
  import { PIT_DATA, PIT_DEFAULT, PIT_GRAND } from "./pivot-view.js";
19
+ import { parseXsdBoolean } from "../xlsx/xform/xsd-values.js";
19
20
  /**
20
21
  * Reduce one source value to what a cache record holds for it: an index into the field's shared items, or the
21
22
  * value itself when the field has none.
@@ -195,7 +196,17 @@ export function pivotParts(pivot, recordsRelationshipId) {
195
196
  `${METRIC_DISPLAY_NAMES[subtotal]} of ${pivot.cacheFields[field]?.name ?? ""}`
196
197
  };
197
198
  }),
198
- dataCaption: "Values"
199
+ // A loaded table carries its own view strings; a new one has only the default data caption. They were
200
+ // all dropped here, so converting a workbook to XLSB reset every caption the user had typed.
201
+ dataCaption: pivot.dataCaption ?? "Values",
202
+ grandTotalCaption: pivot.grandTotalCaption,
203
+ rowHeaderCaption: pivot.rowHeaderCaption,
204
+ colHeaderCaption: pivot.colHeaderCaption,
205
+ showError: parseXsdBoolean(pivot.showError),
206
+ errorCaption: pivot.errorCaption,
207
+ showMissing: parseXsdBoolean(pivot.showMissing),
208
+ missingCaption: pivot.missingCaption,
209
+ tag: pivot.tag
199
210
  };
200
211
  return { cache, view };
201
212
  }
@@ -11,6 +11,7 @@
11
11
  * and only the declared `0x3B` reveals that the trailing `reserved` is inside the `PivotNumFmt` rather than
12
12
  * beside it.
13
13
  */
14
+ import { encodeWideString } from "./binary.js";
14
15
  import { encodeTableStyleClient } from "./table-style.js";
15
16
  import { BinaryWriter, concatUint8Arrays } from "../../../utils/binary.js";
16
17
  /**
@@ -101,15 +102,6 @@ export const PIT_GRAND = 0x0d;
101
102
  const PIVOT_APP_VERSION = 8;
102
103
  /** `bVerSxUpdateableMin` — the oldest version allowed to update the view. Three, as Excel writes. */
103
104
  const PIVOT_MIN_VERSION = 3;
104
- /** An `XLWideString`: a four-byte character count, then UTF-16. */
105
- function wideString(value) {
106
- const characters = [...value];
107
- const writer = new BinaryWriter().writeUint32(characters.length);
108
- for (const character of characters.join("")) {
109
- writer.writeUint16(character.charCodeAt(0));
110
- }
111
- return writer.toUint8Array();
112
- }
113
105
  /** A counted array of 32-bit field indices, which four of these collections are. */
114
106
  function indexArray(indices) {
115
107
  const writer = new BinaryWriter().writeUint32(indices.length);
@@ -300,13 +292,24 @@ export function pivotViewRecords(view) {
300
292
  /**
301
293
  * `BrtBeginSXView` — MS-XLSB 2.4.278. Thirty-two fixed bytes and up to eleven conditional strings.
302
294
  *
303
- * Three of the flags are **presence** flags for those strings, and two of them are **inverted**:
304
- * `fDisplayData` (bit 25 of the second word) says `irstData` *is* present and MUST be 1, while
305
- * `fEmptyDisplayErrorString` and `fEmptyDisplayNullString` say their strings are *absent*. So a minimal
306
- * record has to *set* the two "empty" bits and clear the rest — reading them as ordinary "has a value" flags
307
- * produces a record whose string fields are read from whatever follows it.
295
+ * Every string after `irstName` has a **presence** flag, and two of them are **inverted**: `fDisplayData`
296
+ * (bit 19 of the second word) says `irstData` *is* present and MUST be 1, while `fEmptyDisplayErrorString`
297
+ * and `fEmptyDisplayNullString` say their strings are *absent*. So a record without those strings has to
298
+ * *set* the two "empty" bits — reading them as ordinary "has a value" flags produces a record whose string
299
+ * fields are read from whatever follows it.
300
+ *
301
+ * Note the specification's own slip: it says `irstColHdrName` is governed by `fUseRwHdrName`. Each header
302
+ * string has its own flag (`fUseColHdrName` for the column one), and the column string comes first.
308
303
  */
309
304
  function viewPayload(view) {
305
+ // The conditional strings, each paired with the flag that declares it. A flag and its string must agree or
306
+ // every later string is read from the wrong offset, so both come from the same `!== undefined` test.
307
+ const grand = limitedCaption(view.grandTotalCaption, SHORT_STRING_LIMIT);
308
+ const error = limitedCaption(view.errorCaption, SHORT_STRING_LIMIT);
309
+ const missing = limitedCaption(view.missingCaption, SHORT_STRING_LIMIT);
310
+ const tag = limitedCaption(view.tag, SHORT_STRING_LIMIT);
311
+ const columnHeader = limitedCaption(view.colHeaderCaption, HEADER_STRING_LIMIT);
312
+ const rowHeader = limitedCaption(view.rowHeaderCaption, HEADER_STRING_LIMIT);
310
313
  // Every value below was read out of an XLSB that Excel wrote for this pivot table. Where a bit's purpose is
311
314
  // known it is named; where it is not, it is still written, because a view Excel will not open is worth less
312
315
  // than a view carrying a flag nobody here can explain.
@@ -325,20 +328,25 @@ function viewPayload(view) {
325
328
  (1 << 6) | // fEnableFieldDialog
326
329
  (1 << 7) | // fPreserveFormatting
327
330
  (1 << 8) | // fAutoFormat
328
- (1 << 10) | // fDisplayNullString
331
+ (view.showError === true ? 1 << 9 : 0) | // fDisplayErrorString
332
+ (view.showMissing === false ? 0 : 1 << 10) | // fDisplayNullString
329
333
  (1 << 13) | // fRwGrand
330
334
  (1 << 14) | // fColGrand
331
335
  (1 << 17) | // fRepeatItemsOnEachPrintedPage
332
336
  (1 << 19) | // fDisplayData — MUST be 1, `irstData` follows
333
- (1 << 29) // ibitAtrProt
337
+ (grand !== undefined ? 1 << 20 : 0) | // fDisplayGrand — `irstGrand` follows
338
+ (1 << 29) | // ibitAtrProt
339
+ (tag !== undefined ? 1 << 30 : 0) // fDisplayTag — `irstTag` follows
334
340
  )
335
341
  // Third flag word, `0x000002D0`. `fCompactData` is **clear**: it was set here to "match what Excel writes
336
342
  // for a new pivot table", and Excel writes it clear for this one — as does this library's XLSX writer,
337
343
  // which declares `compactData="0"` in the file Excel opens.
338
344
  .writeUint32((1 << (36 - 32)) | // fNewDropZones
339
- (1 << (38 - 32)) | // fEmptyDisplayErrorString
340
- (1 << (39 - 32)) | // fEmptyDisplayNullString
341
- (1 << (41 - 32)) // fSingleFilterPerField
345
+ (error === undefined ? 1 << (38 - 32) : 0) | // fEmptyDisplayErrorString — inverted
346
+ (missing === undefined ? 1 << (39 - 32) : 0) | // fEmptyDisplayNullString — inverted
347
+ (1 << (41 - 32)) | // fSingleFilterPerField
348
+ (rowHeader !== undefined ? 1 << (42 - 32) : 0) | // fUseRwHdrName
349
+ (columnHeader !== undefined ? 1 << (43 - 32) : 0) // fUseColHdrName
342
350
  )
343
351
  // `sxaxis4Data`: the axis the data field sits on — see {@link dataFieldAxis} for why this is a constant.
344
352
  .writeUint8(dataFieldAxis())
@@ -356,13 +364,40 @@ function viewPayload(view) {
356
364
  // `dwCrtFmtId`: the next pivot-chart identifier. 0 because no pivot chart is written.
357
365
  .writeUint32(0)
358
366
  .writeUint32(view.cacheId);
367
+ // In the record's order: name, data, grand, error, null, page-field style, table style, vacate style, tag,
368
+ // column header, row header. The three styles are never written, so their flags stay clear.
369
+ const strings = [
370
+ view.name,
371
+ // `irstData`, present because `fDisplayData` is 1.
372
+ limitedCaption(view.dataCaption, SHORT_STRING_LIMIT),
373
+ grand,
374
+ error,
375
+ missing,
376
+ tag,
377
+ columnHeader,
378
+ rowHeader
379
+ ];
359
380
  return concatUint8Arrays([
360
381
  writer.toUint8Array(),
361
- wideString(view.name),
362
- // `irstData`, present because `fDisplayData` is 1.
363
- wideString(view.dataCaption)
382
+ ...strings.flatMap(value => (value === undefined ? [] : [encodeWideString(value)]))
364
383
  ]);
365
384
  }
385
+ /** `XLWideString` limit for the view's captions — 255 characters, as Excel's own user interface enforces. */
386
+ const SHORT_STRING_LIMIT = 255;
387
+ /** `irstRwHdrName` / `irstColHdrName` may be longer when `bVerSxMacro` is 3 or more, which this writer's is. */
388
+ const HEADER_STRING_LIMIT = 32767;
389
+ /**
390
+ * A caption cut to the length the record allows. XLSX has no such limit, so a longer caption from a loaded
391
+ * file has to be shortened rather than written into a record a reader is entitled to reject; the cut is made
392
+ * on a code-unit boundary that does not split a surrogate pair.
393
+ */
394
+ function limitedCaption(value, limit) {
395
+ if (value === undefined || value.length <= limit) {
396
+ return value;
397
+ }
398
+ const end = /[\uD800-\uDBFF]/.test(value[limit - 1]) ? limit - 1 : limit;
399
+ return value.slice(0, end);
400
+ }
366
401
  /**
367
402
  * `BrtBeginSXLocation` — MS-XLSB 2.4.259. The body's extent, then four positions inside it.
368
403
  *
@@ -670,6 +705,6 @@ function dataItemPayload(item) {
670
705
  // `fLoadDisplayName` — the caption follows.
671
706
  .writeUint8(1)
672
707
  .toUint8Array(),
673
- wideString(item.caption)
708
+ encodeWideString(item.caption)
674
709
  ]);
675
710
  }
@@ -1,6 +1,7 @@
1
1
  import { pivotError } from "../../../core/pivot-table-types.js";
2
2
  import { BaseXform } from "../base-xform.js";
3
3
  import { RawXmlCollector } from "./raw-xml-collector.js";
4
+ import { parseXsdBoolean } from "../xsd-values.js";
4
5
  import { parseOoxmlDate } from "#platform/utils/utils";
5
6
  /** Attribute keys on <cacheField> that are individually parsed (not collected into extraAttrs). */
6
7
  const KNOWN_CACHE_FIELD_KEYS = new Set(["name", "numFmtId"]);
@@ -189,7 +190,7 @@ function parseSharedItemValue(tag, attributes) {
189
190
  }
190
191
  return parseFloat(attributes.v);
191
192
  case "b":
192
- return attributes.v === "1";
193
+ return parseXsdBoolean(attributes.v) ?? false;
193
194
  case "e":
194
195
  return pivotError(attributes.v ?? "");
195
196
  case "m":
@@ -2,6 +2,7 @@ import { isPivotError } from "../../../core/pivot-table-types.js";
2
2
  import { PivotTableError } from "../../../errors.js";
3
3
  import { BaseXform } from "../base-xform.js";
4
4
  import { formatDateForExcel } from "./cache-field.js";
5
+ import { parseXsdBoolean } from "../xsd-values.js";
5
6
  import { parseOoxmlDate } from "#platform/utils/utils";
6
7
  import { xmlEncode } from "../../../../xml/encode.js";
7
8
  import { StdDocAttributes } from "../../../../xml/writer.js";
@@ -236,7 +237,7 @@ class PivotCacheRecordsXform extends BaseXform {
236
237
  if (this.currentRecord) {
237
238
  this.currentRecord.push({
238
239
  type: "b",
239
- value: attributes.v === "1"
240
+ value: parseXsdBoolean(attributes.v) ?? false
240
241
  });
241
242
  }
242
243
  break;