@shbernal/ts-xlsx 1.2.0 → 1.3.1

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
@@ -236,6 +236,11 @@ npx skills add ./node_modules/@shbernal/ts-xlsx # offline, matches your inst
236
236
  npx skills add shbernal/ts-xlsx # or straight from the repo
237
237
  ```
238
238
 
239
+ That writes the skill under `.agents/skills/`, links it into whichever agent directories it
240
+ finds, and records what it did in `skills-lock.json`. Track the lock file and ignore the
241
+ copies: they are derived, `npx skills experimental_install` rebuilds them from the lock, and
242
+ `npx skills update` overwrites them in place.
243
+
239
244
  It covers triage (is this bug ours or your file's?), reducing a failure to a script that
240
245
  builds its own input, and — because spreadsheets carry real data — never uploading a
241
246
  workbook to a public tracker. Once the reproduction stands on its own, it files without
@@ -89,12 +89,24 @@ export declare const DEFAULT_WORKBOOK_VIEW: {
89
89
  readonly height: 12220;
90
90
  readonly activeTab: 0;
91
91
  };
92
- /** Document-level metadata written to the package's core properties. */
92
+ /**
93
+ * Document-level metadata — what Excel's File ▸ Info panel shows. Mostly the package's core
94
+ * properties (`docProps/core.xml`); `company` is the exception and lives in the extended part,
95
+ * because that is where OOXML puts it. One interface either way: which part a field lands in is
96
+ * the format's business, not the caller's.
97
+ */
93
98
  export interface WorkbookProperties {
99
+ /** `dc:title` — the document's title, as Excel's File ▸ Info shows it. */
100
+ title?: string;
94
101
  creator?: string;
95
102
  lastModifiedBy?: string;
96
103
  created?: Date;
97
104
  modified?: Date;
105
+ /**
106
+ * `Company` in the extended properties (`docProps/app.xml`), not the core ones — the only
107
+ * field here that does not live beside the others, because OOXML puts it in the other part.
108
+ */
109
+ company?: string;
98
110
  }
99
111
  /**
100
112
  * A named reference in the workbook — the entries Excel surfaces in its Name Manager. A name maps
@@ -51,6 +51,15 @@ export interface SheetView {
51
51
  ySplit?: number;
52
52
  /** The cell anchoring the bottom-right scrolling pane; defaults to the first unfrozen cell. */
53
53
  topLeftCell?: string;
54
+ /**
55
+ * Whether the grid's cell lines are drawn on screen. Absent means Excel's default, which is on,
56
+ * so only `false` is worth setting: a sheet that paints its own fills and borders reads as a
57
+ * document rather than a spreadsheet once the grid behind it is off.
58
+ *
59
+ * On screen only, and unrelated to {@link PrintOptions.gridLines}, which decides whether the grid
60
+ * is *printed*. Excel exposes them as two separate checkboxes because the answers differ.
61
+ */
62
+ showGridLines?: boolean;
54
63
  }
55
64
  /**
56
65
  * Per-column formatting. A column may exist purely to carry these, with no cells. The style
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Modification time stamped on every zip entry, so package bytes depend on the workbook alone.
3
+ * Encodes as 2001-01-01 12:00:00 in the DOS date field on any machine, in any timezone.
4
+ */
5
+ export declare const FIXED_ENTRY_MTIME: number;
@@ -0,0 +1 @@
1
+ export const FIXED_ENTRY_MTIME = new Date(2001, 0, 1, 12, 0, 0, 0).getTime();
@@ -3,6 +3,7 @@ import { VbaAuthorError } from '../../vba/errors.js';
3
3
  import { addVbaReference, removeVbaModule, } from '../../vba/project-editor.js';
4
4
  import { relsPathFor } from '../opc/part-paths.js';
5
5
  import { parseRelationshipRecords, relationshipTargetByType, resolveRelativePart, resolveWorkbookPart, } from '../opc/read-opc.js';
6
+ import { FIXED_ENTRY_MTIME } from '../opc/zip-mtime.js';
6
7
  const OFFICE_DOCUMENT_REL = 'officeDocument';
7
8
  const VBA_PROJECT_REL = 'vbaProject';
8
9
  const VBA_SIGNATURE_REL_INFIX = 'vbaProjectSignature';
@@ -21,7 +22,7 @@ function applyToVbaProjectPart(xlsx, apply) {
21
22
  }
22
23
  files[binPath] = apply(bin);
23
24
  dropStaleSignature(files, binPath);
24
- return zipSync(files);
25
+ return zipSync(files, { mtime: FIXED_ENTRY_MTIME });
25
26
  }
26
27
  function locateVbaProjectPart(files) {
27
28
  const rootRels = textPart(files, '_rels/.rels');
@@ -200,6 +200,7 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
200
200
  break;
201
201
  case 'tabColor':
202
202
  case 'outlinePr':
203
+ case 'sheetView':
203
204
  case 'pane':
204
205
  case 'pageSetUpPr':
205
206
  case 'printOptions':
@@ -312,6 +313,11 @@ function applySheetProperties(local, attrs, sheet) {
312
313
  if (attrs.summaryRight !== undefined)
313
314
  sheet.outline.summaryRight = boolPresent(attrs.summaryRight);
314
315
  break;
316
+ case 'sheetView':
317
+ if (attrs.showGridLines !== undefined && !boolPresent(attrs.showGridLines)) {
318
+ sheet.view.showGridLines = false;
319
+ }
320
+ break;
315
321
  case 'pane':
316
322
  if (attrs.state === 'frozen' || attrs.state === 'frozenSplit') {
317
323
  sheet.view.state = 'frozen';
@@ -53,6 +53,9 @@ export function readXlsx(data, options = {}) {
53
53
  const core = partText('docProps/core.xml');
54
54
  if (core !== undefined)
55
55
  applyCoreProperties(workbook, core);
56
+ const app = partText('docProps/app.xml');
57
+ if (app !== undefined)
58
+ applyAppProperties(workbook, app);
56
59
  workbook.protection = parseWorkbookProtection(workbookXml);
57
60
  applyWorkbookView(workbook.view, workbookXml);
58
61
  readWorkbookPersons(workbookRelsXml, pkg, workbook);
@@ -436,7 +439,13 @@ function parseWorkbookDefinedNames(xml, sheetOrder) {
436
439
  });
437
440
  return names;
438
441
  }
439
- const CORE_PROPERTY_LOCAL_NAMES = new Set(['creator', 'lastModifiedBy', 'created', 'modified']);
442
+ const CORE_PROPERTY_LOCAL_NAMES = new Set([
443
+ 'title',
444
+ 'creator',
445
+ 'lastModifiedBy',
446
+ 'created',
447
+ 'modified',
448
+ ]);
440
449
  function applyCoreProperties(workbook, xml) {
441
450
  let capture = '';
442
451
  let text = '';
@@ -453,7 +462,9 @@ function applyCoreProperties(workbook, xml) {
453
462
  onClose(name) {
454
463
  if (capture === '' || localName(name) !== capture)
455
464
  return;
456
- if (capture === 'creator')
465
+ if (capture === 'title')
466
+ workbook.properties.title = text;
467
+ else if (capture === 'creator')
457
468
  workbook.properties.creator = text;
458
469
  else if (capture === 'lastModifiedBy')
459
470
  workbook.properties.lastModifiedBy = text;
@@ -470,3 +481,23 @@ function applyCoreProperties(workbook, xml) {
470
481
  },
471
482
  });
472
483
  }
484
+ function applyAppProperties(workbook, xml) {
485
+ let capture = false;
486
+ let text = '';
487
+ parseXml(xml, {
488
+ onOpen(name) {
489
+ capture = localName(name) === 'Company';
490
+ text = '';
491
+ },
492
+ onText(chunk) {
493
+ if (capture)
494
+ text += chunk;
495
+ },
496
+ onClose(name) {
497
+ if (!capture || localName(name) !== 'Company')
498
+ return;
499
+ workbook.properties.company = text;
500
+ capture = false;
501
+ },
502
+ });
503
+ }
@@ -4,10 +4,11 @@ import { attr, boolAttr, escapeAttr, escapeText, numberText } from '../../xml/xm
4
4
  import { colorAttrs } from './color-xml.js';
5
5
  export function sheetViewsXml(view, active) {
6
6
  const selected = active ? ' tabSelected="1"' : '';
7
+ const gridLines = view.showGridLines === false ? ' showGridLines="0"' : '';
7
8
  const xSplit = view.xSplit ?? 0;
8
9
  const ySplit = view.ySplit ?? 0;
9
10
  if (view.state !== 'frozen' || (xSplit === 0 && ySplit === 0)) {
10
- return `<sheetViews><sheetView${selected} workbookViewId="0"/></sheetViews>`;
11
+ return `<sheetViews><sheetView${gridLines}${selected} workbookViewId="0"/></sheetViews>`;
11
12
  }
12
13
  const topLeftCell = view.topLeftCell ?? encodeAddress(xSplit + 1, ySplit + 1);
13
14
  const activePane = xSplit > 0 && ySplit > 0 ? 'bottomRight' : xSplit > 0 ? 'topRight' : 'bottomLeft';
@@ -16,7 +17,7 @@ export function sheetViewsXml(view, active) {
16
17
  (ySplit > 0 ? ` ySplit="${ySplit}"` : '') +
17
18
  ` topLeftCell="${escapeAttr(topLeftCell)}" activePane="${activePane}" state="frozen"/>`;
18
19
  const selection = `<selection pane="${activePane}" activeCell="${escapeAttr(topLeftCell)}" sqref="${escapeAttr(topLeftCell)}"/>`;
19
- return `<sheetViews><sheetView${selected} workbookViewId="0">${pane}${selection}</sheetView></sheetViews>`;
20
+ return `<sheetViews><sheetView${gridLines}${selected} workbookViewId="0">${pane}${selection}</sheetView></sheetViews>`;
20
21
  }
21
22
  export function sheetPrXml(sheet) {
22
23
  const children = (sheet.tabColor !== undefined ? `<tabColor ${colorAttrs(sheet.tabColor)}/>` : '') +
@@ -9,4 +9,4 @@ export declare function workbookXml(workbook: Workbook, preservedRels: readonly
9
9
  export declare const FIXED_WORKBOOK_REL_COUNT = 2;
10
10
  export declare function workbookRelsXml(sheetCount: number, hasSharedStrings: boolean, personsRelId: string | null, preservedRels: readonly PreservedWorkbookRel[], pivots: readonly PivotPlan[]): string;
11
11
  export declare function corePropsXml(properties: WorkbookProperties): string;
12
- export declare function appPropsXml(): string;
12
+ export declare function appPropsXml(properties: WorkbookProperties): string;
@@ -232,6 +232,9 @@ export function workbookRelsXml(sheetCount, hasSharedStrings, personsRelId, pres
232
232
  }
233
233
  export function corePropsXml(properties) {
234
234
  const parts = [];
235
+ if (properties.title !== undefined) {
236
+ parts.push(`<dc:title>${escapeText(properties.title)}</dc:title>`);
237
+ }
235
238
  if (properties.creator !== undefined) {
236
239
  parts.push(`<dc:creator>${escapeText(properties.creator)}</dc:creator>`);
237
240
  }
@@ -253,7 +256,8 @@ export function corePropsXml(properties) {
253
256
  function w3cdtf(element, date) {
254
257
  return `<dcterms:${element} xsi:type="dcterms:W3CDTF">${date.toISOString()}</dcterms:${element}>`;
255
258
  }
256
- export function appPropsXml() {
259
+ export function appPropsXml(properties) {
260
+ const company = properties.company === undefined ? '' : `<Company>${escapeText(properties.company)}</Company>`;
257
261
  return (XML_DECLARATION +
258
- `<Properties xmlns="${NS.extProps}"><Application>ts-xlsx</Application></Properties>`);
262
+ `<Properties xmlns="${NS.extProps}"><Application>ts-xlsx</Application>${company}</Properties>`);
259
263
  }
@@ -6,6 +6,7 @@ import { INTERNAL } from '../../core/internal.js';
6
6
  import { isSharedFormulaValue } from '../../core/value.js';
7
7
  import { Workbook } from '../../core/workbook.js';
8
8
  import { AuthoringError } from '../../errors.js';
9
+ import { FIXED_ENTRY_MTIME } from '../opc/zip-mtime.js';
9
10
  import { buildColumnDefaults, buildPackageParts, createStyleRegistry, Extent, renderRow, } from './write.js';
10
11
  export class StreamedRow {
11
12
  #cells;
@@ -244,6 +245,7 @@ function streamZipPackage(parts, onChunk) {
244
245
  });
245
246
  for (const [name, data] of Object.entries(parts)) {
246
247
  const entry = new ZipDeflate(name, { level: 6 });
248
+ entry.mtime = FIXED_ENTRY_MTIME;
247
249
  zip.add(entry);
248
250
  entry.push(data, true);
249
251
  }
@@ -37,6 +37,10 @@ export interface InternalWriteOptions extends WriteOptions {
37
37
  /**
38
38
  * Serialise a workbook into an `.xlsx` package.
39
39
  *
40
+ * The bytes are a pure function of the workbook: an unchanged model written twice produces two
41
+ * identical archives, because entry timestamps are pinned to a fixed date rather than taken from the
42
+ * clock. A committed `.xlsx` therefore only changes when something about it changed.
43
+ *
40
44
  * @throws {AuthoringError} if the workbook has no worksheets (a zero-sheet package is corrupt),
41
45
  * or holds a value the writer cannot yet represent.
42
46
  */
@@ -44,7 +48,7 @@ export declare function writeXlsx(workbook: Workbook, options?: WriteOptions): U
44
48
  /**
45
49
  * Serialise a workbook into an `.xlsx` package, deflating off the calling thread.
46
50
  *
47
- * Produces the same package {@link writeXlsx} does — every part compresses to identical bytes — and
51
+ * Produces the same package {@link writeXlsx} does — byte for byte, entry timestamps included — and
48
52
  * exists for one reason: DEFLATE dominates the cost of writing a large workbook, and {@link writeXlsx}
49
53
  * spends all of it on the caller's thread. Here `fflate` deflates each part in a worker, so the event
50
54
  * loop keeps turning (stalls drop from the whole write to tens of milliseconds) and parts compress in
@@ -3,6 +3,7 @@ import { DEFAULT_THEME_XML } from '../../core/theme.js';
3
3
  import { AuthoringError } from '../../errors.js';
4
4
  import { THEME_PART_PATH } from '../opc/part-paths.js';
5
5
  import { relsPartXml } from '../opc/rels.js';
6
+ import { FIXED_ENTRY_MTIME } from '../opc/zip-mtime.js';
6
7
  import { collectComments, commentsXml, vmlDrawingXml } from './comments.js';
7
8
  import { collectHyperlinks, planHyperlinks } from './hyperlinks.js';
8
9
  import { drawingRelsXml, drawingXml } from './images.js';
@@ -17,12 +18,12 @@ import { appPropsXml, contentTypesXml, corePropsXml, FIXED_WORKBOOK_REL_COUNT, r
17
18
  import { worksheetRelsXml, worksheetXml, } from './worksheet-xml.js';
18
19
  export { buildColumnDefaults, Extent, renderRow, } from './worksheet-xml.js';
19
20
  export function writeXlsx(workbook, options = {}) {
20
- return zipSync(buildPackageParts(workbook, options), { level: 6 });
21
+ return zipSync(buildPackageParts(workbook, options), { level: 6, mtime: FIXED_ENTRY_MTIME });
21
22
  }
22
23
  export async function writeXlsxAsync(workbook, options = {}) {
23
24
  const parts = buildPackageParts(workbook, options);
24
25
  return await new Promise((resolve, reject) => {
25
- zip(parts, { level: 6 }, (error, data) => {
26
+ zip(parts, { level: 6, mtime: FIXED_ENTRY_MTIME }, (error, data) => {
26
27
  if (error)
27
28
  reject(error);
28
29
  else
@@ -177,7 +178,7 @@ export function buildPackageParts(workbook, options = {}) {
177
178
  '[Content_Types].xml': strToU8(contentTypesXml(sheets.length, allTables, commentNumbers, drawingNumbers, printerSettingsNumbers, media.extensions, hasSharedStrings, preserved.parts, allPivots, preservedWorkbookRels, threadedCommentNumbers, persons.length > 0)),
178
179
  '_rels/.rels': strToU8(rootRelsXml(preserved.root)),
179
180
  'docProps/core.xml': strToU8(corePropsXml(workbook.properties)),
180
- 'docProps/app.xml': strToU8(appPropsXml()),
181
+ 'docProps/app.xml': strToU8(appPropsXml(workbook.properties)),
181
182
  'xl/workbook.xml': strToU8(workbookXml(workbook, preservedWorkbookRels, allPivots)),
182
183
  'xl/_rels/workbook.xml.rels': strToU8(workbookRelsXml(sheets.length, hasSharedStrings, personsRelId, preservedWorkbookRels, allPivots)),
183
184
  'xl/styles.xml': strToU8(styles.toXml()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shbernal/ts-xlsx",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "A TypeScript-first library for reading and writing xlsx (OOXML) spreadsheets.",
5
5
  "license": "MIT",
6
6
  "author": "shbernal",