@shbernal/ts-xlsx 2.0.0 → 3.0.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.
Files changed (147) hide show
  1. package/README.md +20 -10
  2. package/dist/bytes.d.ts +24 -0
  3. package/dist/bytes.js +34 -0
  4. package/dist/core/address.d.ts +26 -4
  5. package/dist/core/address.js +21 -9
  6. package/dist/core/autofilter.d.ts +12 -1
  7. package/dist/core/autofilter.js +35 -12
  8. package/dist/core/axis-handle.d.ts +8 -0
  9. package/dist/core/axis-handle.js +14 -0
  10. package/dist/core/cell.d.ts +24 -0
  11. package/dist/core/cell.js +9 -1
  12. package/dist/core/color-resolution.d.ts +1 -1
  13. package/dist/core/color-resolution.js +7 -8
  14. package/dist/core/column.d.ts +4 -1
  15. package/dist/core/column.js +31 -35
  16. package/dist/core/conditional-formatting-overlay.d.ts +5 -0
  17. package/dist/core/conditional-formatting-overlay.js +11 -0
  18. package/dist/core/conditional-formatting.d.ts +45 -5
  19. package/dist/core/conditional-formatting.js +74 -0
  20. package/dist/core/data-validation-overlay.d.ts +11 -0
  21. package/dist/core/data-validation-overlay.js +21 -2
  22. package/dist/core/data-validation.d.ts +9 -2
  23. package/dist/core/data-validation.js +26 -0
  24. package/dist/core/grid-edits.d.ts +16 -0
  25. package/dist/core/grid-edits.js +26 -8
  26. package/dist/core/grid-shift.d.ts +19 -0
  27. package/dist/core/grid-shift.js +8 -0
  28. package/dist/core/image.d.ts +2 -0
  29. package/dist/core/image.js +2 -0
  30. package/dist/core/merge-index.d.ts +31 -0
  31. package/dist/core/merge-index.js +60 -0
  32. package/dist/core/merge.d.ts +12 -11
  33. package/dist/core/merge.js +36 -10
  34. package/dist/core/page-setup.d.ts +86 -2
  35. package/dist/core/page-setup.js +34 -1
  36. package/dist/core/pivot-table.d.ts +2 -0
  37. package/dist/core/pivot-table.js +23 -18
  38. package/dist/core/protection.d.ts +3 -0
  39. package/dist/core/protection.js +20 -14
  40. package/dist/core/row-input.d.ts +7 -0
  41. package/dist/core/row-input.js +10 -8
  42. package/dist/core/row.d.ts +4 -1
  43. package/dist/core/row.js +19 -23
  44. package/dist/core/style.d.ts +78 -7
  45. package/dist/core/style.js +90 -80
  46. package/dist/core/table.d.ts +26 -12
  47. package/dist/core/table.js +67 -33
  48. package/dist/core/theme.d.ts +3 -1
  49. package/dist/core/theme.js +4 -0
  50. package/dist/core/used-extent.d.ts +44 -0
  51. package/dist/core/used-extent.js +102 -0
  52. package/dist/core/workbook.d.ts +2 -2
  53. package/dist/core/worksheet-comments.d.ts +8 -0
  54. package/dist/core/worksheet-comments.js +18 -0
  55. package/dist/core/worksheet.d.ts +31 -7
  56. package/dist/core/worksheet.js +72 -86
  57. package/dist/customui/ribbon.js +23 -24
  58. package/dist/entries/core.d.ts +3 -3
  59. package/dist/entries/csv.d.ts +1 -1
  60. package/dist/entries/node-unavailable.d.ts +15 -0
  61. package/dist/entries/node-unavailable.js +20 -0
  62. package/dist/entries/node.d.ts +1 -0
  63. package/dist/entries/node.js +1 -0
  64. package/dist/entries/xlsx.d.ts +0 -1
  65. package/dist/entries/xlsx.js +0 -1
  66. package/dist/io/csv/read.js +1 -1
  67. package/dist/io/csv/write.d.ts +12 -2
  68. package/dist/io/csv/write.js +17 -2
  69. package/dist/io/opc/inflate.js +3 -13
  70. package/dist/io/opc/part-paths.d.ts +1 -0
  71. package/dist/io/opc/part-paths.js +15 -0
  72. package/dist/io/opc/read-opc.d.ts +22 -3
  73. package/dist/io/opc/read-opc.js +11 -39
  74. package/dist/io/opc/rels.d.ts +12 -6
  75. package/dist/io/opc/rels.js +3 -5
  76. package/dist/io/style/xf-style.d.ts +33 -4
  77. package/dist/io/style/xf-style.js +12 -1
  78. package/dist/io/xlsb/read-styles.js +6 -3
  79. package/dist/io/xlsb/read.d.ts +1 -1
  80. package/dist/io/xlsb/read.js +5 -8
  81. package/dist/io/xlsx/cell-accumulator.d.ts +2 -4
  82. package/dist/io/xlsx/cell-accumulator.js +20 -58
  83. package/dist/io/xlsx/cell-value.d.ts +1 -1
  84. package/dist/io/xlsx/cell-value.js +7 -4
  85. package/dist/io/xlsx/color-xml.d.ts +1 -1
  86. package/dist/io/xlsx/color-xml.js +8 -7
  87. package/dist/io/xlsx/comments.js +6 -3
  88. package/dist/io/xlsx/conditional-formatting.d.ts +4 -3
  89. package/dist/io/xlsx/conditional-formatting.js +81 -65
  90. package/dist/io/xlsx/data-validation.d.ts +10 -8
  91. package/dist/io/xlsx/data-validation.js +44 -41
  92. package/dist/io/xlsx/edit-vba.js +4 -11
  93. package/dist/io/xlsx/hyperlinks.d.ts +4 -2
  94. package/dist/io/xlsx/hyperlinks.js +26 -31
  95. package/dist/io/xlsx/images.js +33 -35
  96. package/dist/io/xlsx/package-plan.d.ts +9 -1
  97. package/dist/io/xlsx/package-plan.js +29 -16
  98. package/dist/io/xlsx/part-names.d.ts +41 -0
  99. package/dist/io/xlsx/part-names.js +46 -0
  100. package/dist/io/xlsx/read-pivot.js +4 -9
  101. package/dist/io/xlsx/read-rows.js +12 -13
  102. package/dist/io/xlsx/read-shared-strings.js +9 -47
  103. package/dist/io/xlsx/read-styles.d.ts +1 -1
  104. package/dist/io/xlsx/read-styles.js +65 -43
  105. package/dist/io/xlsx/read-worksheet.d.ts +10 -1
  106. package/dist/io/xlsx/read-worksheet.js +48 -63
  107. package/dist/io/xlsx/read.d.ts +3 -4
  108. package/dist/io/xlsx/read.js +124 -146
  109. package/dist/io/xlsx/rich-runs.d.ts +41 -7
  110. package/dist/io/xlsx/rich-runs.js +95 -30
  111. package/dist/io/xlsx/sheet-properties.d.ts +1 -1
  112. package/dist/io/xlsx/sheet-properties.js +23 -26
  113. package/dist/io/xlsx/styles.d.ts +3 -15
  114. package/dist/io/xlsx/styles.js +83 -105
  115. package/dist/io/xlsx/tables.js +14 -13
  116. package/dist/io/xlsx/theme-xml.js +8 -5
  117. package/dist/io/xlsx/threaded-comments.js +20 -19
  118. package/dist/io/xlsx/workbook-xml.js +43 -36
  119. package/dist/io/xlsx/worksheet-xml.d.ts +1 -1
  120. package/dist/io/xlsx/worksheet-xml.js +47 -52
  121. package/dist/io/xlsx/write-stream.d.ts +14 -1
  122. package/dist/io/xlsx/write-stream.js +40 -33
  123. package/dist/io/xlsx/write.d.ts +5 -0
  124. package/dist/io/xlsx/write.js +155 -131
  125. package/dist/sha512.d.ts +2 -0
  126. package/dist/sha512.js +146 -0
  127. package/dist/token-set.d.ts +15 -0
  128. package/dist/token-set.js +4 -0
  129. package/dist/vba/bytes.d.ts +1 -2
  130. package/dist/vba/bytes.js +1 -12
  131. package/dist/vba/cfb-format.d.ts +24 -0
  132. package/dist/vba/cfb-format.js +12 -0
  133. package/dist/vba/cfb-writer.js +17 -17
  134. package/dist/vba/cfb.js +1 -7
  135. package/dist/vba/dir-records.d.ts +53 -0
  136. package/dist/vba/dir-records.js +28 -0
  137. package/dist/vba/ms-ovba.js +32 -11
  138. package/dist/vba/project-editor.js +5 -34
  139. package/dist/vba/project.js +15 -31
  140. package/dist/vba/vba-encoding.js +1 -1
  141. package/dist/xml/xml-read.d.ts +117 -72
  142. package/dist/xml/xml-read.js +123 -169
  143. package/dist/xml/xml-scan.d.ts +100 -0
  144. package/dist/xml/xml-scan.js +208 -0
  145. package/dist/xml/xml.d.ts +44 -2
  146. package/dist/xml/xml.js +17 -5
  147. package/package.json +22 -6
@@ -1,26 +1,66 @@
1
1
  import type { Color, DifferentialStyle } from './style.ts';
2
+ /** How a {@link CfValueObject} reads its `value`: `ST_CfvoType` verbatim. */
3
+ export type CfValueObjectType = 'num' | 'percent' | 'max' | 'min' | 'percentile' | 'formula';
4
+ /** Narrow a raw `<cfvo type>` token to a known {@link CfValueObjectType}. */
5
+ export declare const isCfValueObjectType: (value: string) => value is CfValueObjectType;
2
6
  /**
3
7
  * One anchor of a colour-scale, data-bar, or icon-set scale: a "conditional format value object".
4
8
  * `type` names how `value` is read: a literal `num`, a `percent`/`percentile` of the range, a
5
9
  * `formula`, or the range's own `min`/`max` (which carry no value).
6
10
  */
7
11
  export interface CfValueObject {
8
- type: 'num' | 'percent' | 'max' | 'min' | 'percentile' | 'formula';
12
+ type: CfValueObjectType;
9
13
  value?: number | string;
10
14
  }
15
+ /**
16
+ * What a rule tests, as `<cfRule type>` carries it: `ST_CfType` verbatim.
17
+ *
18
+ * Closed, and stated in full rather than left as `string`, even though the library models only some
19
+ * of these in depth. Depth of modelling and legality are different questions: a `timePeriod` rule
20
+ * whose operands the library never inspects still round-trips, while a token outside this list is one
21
+ * Excel refuses to open, so it is refused on the way in and on the way out alike.
22
+ */
23
+ export type ConditionalFormattingType = 'expression' | 'cellIs' | 'colorScale' | 'dataBar' | 'iconSet' | 'top10' | 'uniqueValues' | 'duplicateValues' | 'containsText' | 'notContainsText' | 'beginsWith' | 'endsWith' | 'containsBlanks' | 'notContainsBlanks' | 'containsErrors' | 'notContainsErrors' | 'timePeriod' | 'aboveAverage';
24
+ /** Narrow a raw `<cfRule type>` token to a known {@link ConditionalFormattingType}. */
25
+ export declare const isConditionalFormattingType: (value: string) => value is ConditionalFormattingType;
26
+ /**
27
+ * How a `cellIs` or text rule compares, as `ST_ConditionalFormattingOperator` enumerates it.
28
+ *
29
+ * Overlaps {@link import('./data-validation.ts').DataValidationOperator} in four members and diverges
30
+ * in the rest: this one has the text comparisons a validation has no use for, and spells "does not
31
+ * contain" as `notContains` where nothing else in the format does.
32
+ */
33
+ export type ConditionalFormattingOperator = 'lessThan' | 'lessThanOrEqual' | 'equal' | 'notEqual' | 'greaterThanOrEqual' | 'greaterThan' | 'between' | 'notBetween' | 'containsText' | 'notContains' | 'beginsWith' | 'endsWith';
34
+ /** Narrow a raw `<cfRule operator>` token to a known {@link ConditionalFormattingOperator}. */
35
+ export declare const isConditionalFormattingOperator: (value: string) => value is ConditionalFormattingOperator;
36
+ /** The window a `timePeriod` rule matches against, relative to the day the sheet is recalculated. */
37
+ export type CfTimePeriod = 'today' | 'yesterday' | 'tomorrow' | 'last7Days' | 'thisMonth' | 'lastMonth' | 'nextMonth' | 'thisWeek' | 'lastWeek' | 'nextWeek';
38
+ /** Narrow a raw `<cfRule timePeriod>` token to a known {@link CfTimePeriod}. */
39
+ export declare const isCfTimePeriod: (value: string) => value is CfTimePeriod;
40
+ /**
41
+ * The named icon family an `iconSet` rule draws from, as `ST_IconSetType` enumerates it. The leading
42
+ * digit is the number of icons, which is also how many {@link CfValueObject} anchors the rule needs.
43
+ *
44
+ * The 2009 extension adds three more families (`3Stars`, `3Triangles`, `5Boxes`) under its own
45
+ * namespace. They are absent here because the classic `<iconSet>` element this list types cannot
46
+ * carry them; a file using one states it in the extension, which the library round-trips verbatim.
47
+ */
48
+ export type IconSetType = '3Arrows' | '3ArrowsGray' | '3Flags' | '3TrafficLights1' | '3TrafficLights2' | '3Signs' | '3Symbols' | '3Symbols2' | '4Arrows' | '4ArrowsGray' | '4RedToBlack' | '4Rating' | '4TrafficLights' | '5Arrows' | '5ArrowsGray' | '5Rating' | '5Quarters';
49
+ /** Narrow a raw `<iconSet iconSet>` token to a known {@link IconSetType}. */
50
+ export declare const isIconSetType: (value: string) => value is IconSetType;
11
51
  /**
12
52
  * A single conditional-formatting rule. `type` is the OOXML cfRule type; the remaining fields carry
13
53
  * the operands that type needs and are absent otherwise. A rule the library does not model in depth
14
54
  * still preserves `type`, `priority`, `operator`, `formulae`, and `dxfId` across a round-trip.
15
55
  */
16
56
  export interface ConditionalFormattingRule {
17
- type: string;
57
+ type: ConditionalFormattingType;
18
58
  /** Evaluation precedence; lower wins. Excel requires one, so the writer supplies it when absent. */
19
59
  priority?: number;
20
60
  /** Halt evaluation of lower-priority rules on any cell this rule matches. */
21
61
  stopIfTrue?: boolean;
22
62
  /** cellIs / text comparison operator (`greaterThan`, `between`, `beginsWith`, …). */
23
- operator?: string;
63
+ operator?: ConditionalFormattingOperator;
24
64
  /** Formula operands: cellIs bounds, an expression predicate, a containsText target formula, … */
25
65
  formulae?: (string | number)[];
26
66
  /** The literal a containsText / beginsWith / endsWith rule searches for. */
@@ -42,7 +82,7 @@ export interface ConditionalFormattingRule {
42
82
  /** A dataBar's axis colour (the zero line between positive and negative bars). An x14 property. */
43
83
  axisColor?: Color;
44
84
  /** An iconSet's named icon family (e.g. `3TrafficLights1`). */
45
- iconSet?: string;
85
+ iconSet?: IconSetType;
46
86
  /** top10 rank cutoff. */
47
87
  rank?: number;
48
88
  /** top10: the rank is a percentage rather than a count. */
@@ -56,7 +96,7 @@ export interface ConditionalFormattingRule {
56
96
  /** aboveAverage: match beyond this many standard deviations. */
57
97
  stdDev?: number;
58
98
  /** timePeriod window (`today`, `lastWeek`, …). */
59
- timePeriod?: string;
99
+ timePeriod?: CfTimePeriod;
60
100
  }
61
101
  /** A set of rules bound to the range(s) they cover. `ref` is an OOXML `sqref`: one or more
62
102
  * space-separated areas (`"A1:C1 A3:C3 A5:C5"`), the shape Excel writes when one rule is applied to
@@ -1,3 +1,77 @@
1
+ import { tokenSet } from '../token-set.js';
2
+ export const isCfValueObjectType = tokenSet({
3
+ num: true,
4
+ percent: true,
5
+ max: true,
6
+ min: true,
7
+ percentile: true,
8
+ formula: true,
9
+ });
10
+ export const isConditionalFormattingType = tokenSet({
11
+ expression: true,
12
+ cellIs: true,
13
+ colorScale: true,
14
+ dataBar: true,
15
+ iconSet: true,
16
+ top10: true,
17
+ uniqueValues: true,
18
+ duplicateValues: true,
19
+ containsText: true,
20
+ notContainsText: true,
21
+ beginsWith: true,
22
+ endsWith: true,
23
+ containsBlanks: true,
24
+ notContainsBlanks: true,
25
+ containsErrors: true,
26
+ notContainsErrors: true,
27
+ timePeriod: true,
28
+ aboveAverage: true,
29
+ });
30
+ export const isConditionalFormattingOperator = tokenSet({
31
+ lessThan: true,
32
+ lessThanOrEqual: true,
33
+ equal: true,
34
+ notEqual: true,
35
+ greaterThanOrEqual: true,
36
+ greaterThan: true,
37
+ between: true,
38
+ notBetween: true,
39
+ containsText: true,
40
+ notContains: true,
41
+ beginsWith: true,
42
+ endsWith: true,
43
+ });
44
+ export const isCfTimePeriod = tokenSet({
45
+ today: true,
46
+ yesterday: true,
47
+ tomorrow: true,
48
+ last7Days: true,
49
+ thisMonth: true,
50
+ lastMonth: true,
51
+ nextMonth: true,
52
+ thisWeek: true,
53
+ lastWeek: true,
54
+ nextWeek: true,
55
+ });
56
+ export const isIconSetType = tokenSet({
57
+ '3Arrows': true,
58
+ '3ArrowsGray': true,
59
+ '3Flags': true,
60
+ '3TrafficLights1': true,
61
+ '3TrafficLights2': true,
62
+ '3Signs': true,
63
+ '3Symbols': true,
64
+ '3Symbols2': true,
65
+ '4Arrows': true,
66
+ '4ArrowsGray': true,
67
+ '4RedToBlack': true,
68
+ '4Rating': true,
69
+ '4TrafficLights': true,
70
+ '5Arrows': true,
71
+ '5ArrowsGray': true,
72
+ '5Rating': true,
73
+ '5Quarters': true,
74
+ });
1
75
  export function cloneConditionalFormatting(cf) {
2
76
  return { ref: cf.ref, rules: cf.rules.map(cloneRule) };
3
77
  }
@@ -11,6 +11,11 @@ export declare class DataValidationOverlay {
11
11
  * (`<x14:dataValidation>`), the carrier Excel uses for a list source on another sheet and other
12
12
  * shapes the standard element cannot express. The reader sets it for a rule found in that form so a
13
13
  * round-trip writes it back there instead of silently corrupting the cross-sheet reference.
14
+ *
15
+ * @throws {AuthoringError} when `sqref` names no area at all. A rule attached to nothing covers no
16
+ * cell and is written back as the same unreadable text, so it is a mistake worth surfacing at the
17
+ * call. The reader does not reach this: it drops such an entry at its own boundary, where a foreign
18
+ * file's malformed attribute is supposed to be dropped.
14
19
  */
15
20
  add(sqref: string, rule: DataValidation, options?: {
16
21
  extended?: boolean;
@@ -23,6 +28,12 @@ export declare class DataValidationOverlay {
23
28
  * validations.
24
29
  */
25
30
  at(col: number, row: number): DataValidation | undefined;
31
+ /**
32
+ * Re-anchor every rule through a row or column splice, so a dropdown stays on the cells it was
33
+ * attached to rather than on whatever moved into their place. A rule whose every target area fell
34
+ * inside a deleted span is dropped with them.
35
+ */
36
+ shift(axis: 'row' | 'col', start: number, count: number, delta: number): void;
26
37
  /** Drop every validation, leaving the overlay empty. */
27
38
  clear(): void;
28
39
  }
@@ -1,15 +1,21 @@
1
+ import { AuthoringError } from '../errors.js';
2
+ import { replaceContents } from './containers.js';
1
3
  import { cloneDataValidation, } from './data-validation.js';
2
- import { decodeSqrefRects } from './merge.js';
4
+ import { decodeSqrefRects, shiftSqref } from './merge.js';
3
5
  export class DataValidationOverlay {
4
6
  #entries = [];
5
7
  #rects = [];
6
8
  add(sqref, rule, options = {}) {
9
+ const rects = decodeSqrefRects(sqref);
10
+ if (rects.length === 0) {
11
+ throw new AuthoringError(`data validation range "${sqref}" names no cells`);
12
+ }
7
13
  const stored = cloneDataValidation(rule);
8
14
  const entry = { sqref, rule: stored };
9
15
  if (options.extended)
10
16
  entry.extended = true;
11
17
  this.#entries.push(entry);
12
- this.#rects.push({ rects: decodeSqrefRects(sqref), rule: stored });
18
+ this.#rects.push({ rects, rule: stored });
13
19
  }
14
20
  get entries() {
15
21
  return this.#entries;
@@ -24,6 +30,19 @@ export class DataValidationOverlay {
24
30
  }
25
31
  return undefined;
26
32
  }
33
+ shift(axis, start, count, delta) {
34
+ const entries = [];
35
+ const rects = [];
36
+ for (const entry of this.#entries) {
37
+ const sqref = shiftSqref(entry.sqref, axis, start, count, delta);
38
+ if (sqref === undefined)
39
+ continue;
40
+ entries.push({ ...entry, sqref });
41
+ rects.push({ rects: decodeSqrefRects(sqref), rule: entry.rule });
42
+ }
43
+ replaceContents(this.#entries, entries);
44
+ replaceContents(this.#rects, rects);
45
+ }
27
46
  clear() {
28
47
  this.#entries.length = 0;
29
48
  this.#rects.length = 0;
@@ -1,11 +1,18 @@
1
1
  /** The kind of constraint a validation enforces. `list` is a dropdown; `custom` is an arbitrary
2
- * boolean formula; the rest bound a typed value (`whole`/`decimal`/`date`/`time`/`textLength`). */
3
- export type DataValidationType = 'list' | 'whole' | 'decimal' | 'date' | 'time' | 'textLength' | 'custom';
2
+ * boolean formula; `none` constrains nothing and exists only to carry the rule's messages; the rest
3
+ * bound a typed value (`whole`/`decimal`/`date`/`time`/`textLength`). */
4
+ export type DataValidationType = 'none' | 'list' | 'whole' | 'decimal' | 'date' | 'time' | 'textLength' | 'custom';
5
+ /** Narrow a raw `<dataValidation type>` token to a known {@link DataValidationType}. */
6
+ export declare const isDataValidationType: (value: string) => value is DataValidationType;
4
7
  /** How a typed validation compares its operand(s). Absent on a `list`/`custom` rule; defaults to
5
8
  * `between` on a typed rule (the value Excel omits from the XML). */
6
9
  export type DataValidationOperator = 'between' | 'notBetween' | 'equal' | 'notEqual' | 'greaterThan' | 'lessThan' | 'greaterThanOrEqual' | 'lessThanOrEqual';
10
+ /** Narrow a raw `<dataValidation operator>` token to a known {@link DataValidationOperator}. */
11
+ export declare const isDataValidationOperator: (value: string) => value is DataValidationOperator;
7
12
  /** How Excel reacts to input that fails the rule. */
8
13
  export type DataValidationErrorStyle = 'stop' | 'warning' | 'information';
14
+ /** Narrow a raw `<dataValidation errorStyle>` token to a known {@link DataValidationErrorStyle}. */
15
+ export declare const isDataValidationErrorStyle: (value: string) => value is DataValidationErrorStyle;
9
16
  /** One validation rule. `formulae` holds the operand(s), `formula1` then optional `formula2`: a
10
17
  * numeric literal is stored as a number, while a cell reference, defined name, or list source keeps
11
18
  * its verbatim string. */
@@ -1,3 +1,29 @@
1
+ import { tokenSet } from '../token-set.js';
2
+ export const isDataValidationType = tokenSet({
3
+ none: true,
4
+ list: true,
5
+ whole: true,
6
+ decimal: true,
7
+ date: true,
8
+ time: true,
9
+ textLength: true,
10
+ custom: true,
11
+ });
12
+ export const isDataValidationOperator = tokenSet({
13
+ between: true,
14
+ notBetween: true,
15
+ equal: true,
16
+ notEqual: true,
17
+ greaterThan: true,
18
+ lessThan: true,
19
+ greaterThanOrEqual: true,
20
+ lessThanOrEqual: true,
21
+ });
22
+ export const isDataValidationErrorStyle = tokenSet({
23
+ stop: true,
24
+ warning: true,
25
+ information: true,
26
+ });
1
27
  export function cloneDataValidation(rule) {
2
28
  return {
3
29
  ...rule,
@@ -1,9 +1,21 @@
1
+ import { type AutoFilter } from './autofilter.ts';
1
2
  import { Cell } from './cell.ts';
3
+ import type { ConditionalFormattingOverlay } from './conditional-formatting-overlay.ts';
4
+ import type { DataValidationOverlay } from './data-validation-overlay.ts';
2
5
  import { type AnchoredImage } from './image.ts';
3
6
  import type { MergeRect } from './merge.ts';
4
7
  import type { Table } from './table.ts';
5
8
  import { type CellValue } from './value.ts';
9
+ import type { WorksheetComments } from './worksheet-comments.ts';
6
10
  import type { ColumnProperties, RowProperties } from './worksheet.ts';
11
+ /**
12
+ * The sheet's autofilter, reached as a slot rather than held by reference like the containers beside
13
+ * it: it is a single replaceable value, and a splice that deletes every filtered line clears it.
14
+ */
15
+ export interface AutoFilterSlot {
16
+ get(): AutoFilter | undefined;
17
+ set(next: AutoFilter | undefined): void;
18
+ }
7
19
  interface GridStorage {
8
20
  readonly rows: Map<number, Map<number, Cell>>;
9
21
  readonly rowProperties: Map<number, RowProperties>;
@@ -12,6 +24,10 @@ interface GridStorage {
12
24
  readonly mergeRects: MergeRect[];
13
25
  readonly tables: Table[];
14
26
  readonly images: AnchoredImage[];
27
+ readonly dataValidations: DataValidationOverlay;
28
+ readonly conditionalFormattings: ConditionalFormattingOverlay;
29
+ readonly comments: WorksheetComments;
30
+ readonly autoFilter: AutoFilterSlot;
15
31
  }
16
32
  export declare class GridEdits {
17
33
  #private;
@@ -1,11 +1,10 @@
1
1
  import { decodeRange, encodeAddress, tryDecodeCellRef } from './address.js';
2
+ import { shiftAutoFilter } from './autofilter.js';
2
3
  import { Cell, copyCellContent } from './cell.js';
3
4
  import { replaceContents } from './containers.js';
5
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
4
6
  import { isOneCellAnchor } from './image.js';
5
7
  import { isSharedFormulaValue } from './value.js';
6
- function shiftIndex(v, start, count, delta) {
7
- return v < start ? v : v >= start + count ? v + delta : start;
8
- }
9
8
  export class GridEdits {
10
9
  #rows;
11
10
  #rowProperties;
@@ -14,6 +13,10 @@ export class GridEdits {
14
13
  #mergeRects;
15
14
  #tables;
16
15
  #images;
16
+ #dataValidations;
17
+ #conditionalFormattings;
18
+ #comments;
19
+ #autoFilter;
17
20
  constructor(storage) {
18
21
  this.#rows = storage.rows;
19
22
  this.#rowProperties = storage.rowProperties;
@@ -22,6 +25,10 @@ export class GridEdits {
22
25
  this.#mergeRects = storage.mergeRects;
23
26
  this.#tables = storage.tables;
24
27
  this.#images = storage.images;
28
+ this.#dataValidations = storage.dataValidations;
29
+ this.#conditionalFormattings = storage.conditionalFormattings;
30
+ this.#comments = storage.comments;
31
+ this.#autoFilter = storage.autoFilter;
25
32
  }
26
33
  spliceRows(start, count, inserted) {
27
34
  const delta = inserted.length - count;
@@ -43,6 +50,7 @@ export class GridEdits {
43
50
  this.#shiftTables('row', start, count, delta);
44
51
  this.#shiftImages('row', start, count, delta);
45
52
  this.#reanchorSharedFormulas('row', start, count, delta);
53
+ this.#shiftRangeBoundOverlays('row', start, count, delta);
46
54
  }
47
55
  spliceColumns(start, count, inserts) {
48
56
  const delta = inserts.length - count;
@@ -74,6 +82,16 @@ export class GridEdits {
74
82
  this.#shiftTables('col', start, count, delta);
75
83
  this.#shiftImages('col', start, count, delta);
76
84
  this.#reanchorSharedFormulas('col', start, count, delta);
85
+ this.#shiftRangeBoundOverlays('col', start, count, delta);
86
+ }
87
+ #shiftRangeBoundOverlays(axis, start, count, delta) {
88
+ this.#dataValidations.shift(axis, start, count, delta);
89
+ this.#conditionalFormattings.shift(axis, start, count, delta);
90
+ this.#comments.shift(axis, start, count, delta);
91
+ const filter = this.#autoFilter.get();
92
+ if (filter !== undefined) {
93
+ this.#autoFilter.set(shiftAutoFilter(filter, axis, start, count, delta));
94
+ }
77
95
  }
78
96
  #relocateRow(cols, destRow) {
79
97
  const moved = new Map();
@@ -99,8 +117,8 @@ export class GridEdits {
99
117
  if (master === undefined)
100
118
  continue;
101
119
  const anchored = axis === 'row'
102
- ? encodeAddress(master.col, shiftIndex(master.row, start, count, delta))
103
- : encodeAddress(shiftIndex(master.col, start, count, delta), master.row);
120
+ ? encodeAddress(master.col, shiftIndex(master.row, start, count, delta, 'row'))
121
+ : encodeAddress(shiftIndex(master.col, start, count, delta, 'col'), master.row);
104
122
  if (anchored === value.sharedFormula)
105
123
  continue;
106
124
  const reanchored = { ...value, sharedFormula: anchored };
@@ -121,7 +139,7 @@ export class GridEdits {
121
139
  map.set(index, value);
122
140
  }
123
141
  #shiftMerges(axis, start, count, delta) {
124
- const shift = (v) => shiftIndex(v, start, count, delta);
142
+ const shift = (v) => shiftIndex(v, start, count, delta, axis);
125
143
  const merges = [];
126
144
  const rects = [];
127
145
  for (const range of this.#merges) {
@@ -131,7 +149,7 @@ export class GridEdits {
131
149
  continue;
132
150
  }
133
151
  const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
134
- if (lo >= start && hi < start + count)
152
+ if (isDeletedSpan(lo, hi, start, count))
135
153
  continue;
136
154
  const rect = axis === 'row'
137
155
  ? { top: shift(top), left, bottom: shift(bottom), right }
@@ -151,7 +169,7 @@ export class GridEdits {
151
169
  #shiftImages(axis, start, count, delta) {
152
170
  const shiftPoint = (point) => {
153
171
  const zeroBased = axis === 'row' ? point.row : point.col;
154
- const shifted = shiftIndex(zeroBased + 1, start, count, delta) - 1;
172
+ const shifted = shiftIndex(zeroBased + 1, start, count, delta, axis) - 1;
155
173
  if (shifted === zeroBased)
156
174
  return point;
157
175
  return axis === 'row' ? { ...point, row: shifted } : { ...point, col: shifted };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Where a 1-based coordinate on `axis` lands after `count` lines are deleted at `start` and the
3
+ * replacement lines shift what follows by `delta`: before the edit it stays put, at or after the
4
+ * edited span it shifts, and inside a deleted span it clamps to the cut line. The clamp is the best
5
+ * effort for a geometry straddling the cut; a caller that must instead *drop* what a delete swallowed
6
+ * whole tests {@link isDeletedSpan} first.
7
+ *
8
+ * A shift never leaves the grid: the result is clamped to the axis's last line, so a region already
9
+ * touching the bottom (or the right edge) keeps its edge there instead of naming a line the format
10
+ * has no room for. That shrinks such a region by what it could not move, which is the lesser of the
11
+ * two evils and is what Excel does to the same region on the same edit.
12
+ */
13
+ export declare function shiftIndex(v: number, start: number, count: number, delta: number, axis: 'row' | 'col'): number;
14
+ /**
15
+ * Whether the inclusive span `lo..hi` lies entirely within the `count` lines deleted at `start`: the
16
+ * test that separates "this moved" from "this is gone". A single coordinate is the degenerate span
17
+ * `lo === hi`.
18
+ */
19
+ export declare function isDeletedSpan(lo: number, hi: number, start: number, count: number): boolean;
@@ -0,0 +1,8 @@
1
+ import { MAX_COLUMN, MAX_ROW } from './address.js';
2
+ export function shiftIndex(v, start, count, delta, axis) {
3
+ const moved = v < start ? v : v >= start + count ? v + delta : start;
4
+ return Math.min(moved, axis === 'row' ? MAX_ROW : MAX_COLUMN);
5
+ }
6
+ export function isDeletedSpan(lo, hi, start, count) {
7
+ return lo >= start && hi < start + count;
8
+ }
@@ -17,6 +17,8 @@ export declare const PX_TO_EMU = 9525;
17
17
  * them; `oneCell` moves but keeps its size; `absolute` is pinned to the page and does neither. Excel
18
18
  * defaults to `oneCell` when the attribute is omitted. */
19
19
  export type ImageEditAs = 'oneCell' | 'twoCell' | 'absolute';
20
+ /** Narrow a raw `<xdr:twoCellAnchor editAs>` token to a known {@link ImageEditAs}. */
21
+ export declare const isImageEditAs: (value: string) => value is ImageEditAs;
20
22
  /** A fixed image size in EMUs: the extent of a one-cell anchor, which pixel dimensions convert into
21
23
  * via {@link PX_TO_EMU}. */
22
24
  export interface Extent {
@@ -1,4 +1,6 @@
1
+ import { tokenSet } from '../token-set.js';
1
2
  export const PX_TO_EMU = 9525;
3
+ export const isImageEditAs = tokenSet({ oneCell: true, twoCell: true, absolute: true });
2
4
  export function isOneCellAnchor(anchor) {
3
5
  return 'ext' in anchor;
4
6
  }
@@ -0,0 +1,31 @@
1
+ import { type GridRect } from './address.ts';
2
+ import type { MergeRect } from './merge.ts';
3
+ /**
4
+ * A row-banded index over a sheet's merged regions. Holds the region list by reference and never
5
+ * mutates it: the sheet stays the owner, reports what it adds, and says when it has rewritten the
6
+ * list wholesale (a splice, an unmerge, a model assignment), which the index answers by rebuilding
7
+ * on the next query rather than by tracking the edit.
8
+ */
9
+ export declare class MergeIndex {
10
+ #private;
11
+ constructor(rects: readonly MergeRect[]);
12
+ /** Note a region added to the list. Ignored while stale: the rebuild reads the list itself. */
13
+ note(rect: MergeRect): void;
14
+ /** Report that the region list has been rewritten from outside this index. */
15
+ invalidate(): void;
16
+ /**
17
+ * The region overlapping `rect`, or `undefined` when the rectangle is free. Which of several
18
+ * overlapping regions is returned is unspecified beyond being one of them; a sheet admits no
19
+ * overlap, so at most one ever applies.
20
+ */
21
+ overlapping(rect: GridRect): MergeRect | undefined;
22
+ /**
23
+ * Resolve a position to the top-left of the merged region covering it, or to itself when no region
24
+ * does. Only fully-bounded regions participate: an unbounded whole-row/column merge carries no
25
+ * rectangle and so resolves nothing.
26
+ */
27
+ masterOf(row: number, col: number): {
28
+ row: number;
29
+ col: number;
30
+ };
31
+ }
@@ -0,0 +1,60 @@
1
+ import { rectsOverlap } from './address.js';
2
+ const BAND_ROWS = 64;
3
+ function bandOf(row) {
4
+ return Math.floor(row / BAND_ROWS);
5
+ }
6
+ export class MergeIndex {
7
+ #rects;
8
+ #bands = new Map();
9
+ #tallest = 0;
10
+ #stale = true;
11
+ constructor(rects) {
12
+ this.#rects = rects;
13
+ }
14
+ note(rect) {
15
+ if (!this.#stale)
16
+ this.#place(rect);
17
+ }
18
+ invalidate() {
19
+ this.#stale = true;
20
+ }
21
+ overlapping(rect) {
22
+ this.#refresh();
23
+ const first = bandOf(Math.max(1, rect.top - this.#tallest + 1));
24
+ const last = bandOf(rect.bottom);
25
+ for (let band = first; band <= last; band++) {
26
+ const bucket = this.#bands.get(band);
27
+ if (bucket === undefined)
28
+ continue;
29
+ for (const candidate of bucket) {
30
+ if (rectsOverlap(candidate, rect))
31
+ return candidate;
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ masterOf(row, col) {
37
+ const covering = this.overlapping({ top: row, left: col, bottom: row, right: col });
38
+ return covering === undefined ? { row, col } : { row: covering.top, col: covering.left };
39
+ }
40
+ #place(rect) {
41
+ const height = rect.bottom - rect.top + 1;
42
+ if (height > this.#tallest)
43
+ this.#tallest = height;
44
+ const band = bandOf(rect.top);
45
+ const bucket = this.#bands.get(band);
46
+ if (bucket === undefined)
47
+ this.#bands.set(band, [rect]);
48
+ else
49
+ bucket.push(rect);
50
+ }
51
+ #refresh() {
52
+ if (!this.#stale)
53
+ return;
54
+ this.#stale = false;
55
+ this.#bands = new Map();
56
+ this.#tallest = 0;
57
+ for (const rect of this.#rects)
58
+ this.#place(rect);
59
+ }
60
+ }
@@ -2,16 +2,6 @@ import { type GridRect } from './address.ts';
2
2
  import type { Cell } from './cell.ts';
3
3
  /** A merged region, as the {@link GridRect} every range-shaped thing in the library is. */
4
4
  export type MergeRect = GridRect;
5
- /**
6
- * Resolve a position to the master (top-left) of the merged region covering it, or to itself when no
7
- * region does. First covering region wins; `Worksheet.mergeCells` rejects overlaps, so at most one
8
- * region ever applies. Only fully-bounded rects participate: an unbounded whole-row/column merge
9
- * carries no rect and so resolves nothing.
10
- */
11
- export declare function masterOf(rects: readonly MergeRect[], row: number, col: number): {
12
- row: number;
13
- col: number;
14
- };
15
5
  /**
16
6
  * Drop any value already sitting in a merge's covered non-anchor cells, keeping only the top-left
17
7
  * anchor, the collapse Excel performs on merge. A leftover covered value would serialise as a
@@ -21,5 +11,16 @@ export declare function masterOf(rects: readonly MergeRect[], row: number, col:
21
11
  export declare function clearCoveredValues(rows: Map<number, Map<number, Cell>>, rect: MergeRect): void;
22
12
  /** Decode an OOXML `sqref` (one or more space-separated ranges) into containment rectangles. A whole
23
13
  * column or row leaves one axis unbounded, so its missing edges open to `Infinity` rather than
24
- * clamping: a cell anywhere down the column still resolves inside it. */
14
+ * clamping: a cell anywhere down the column still resolves inside it. An area that names no region
15
+ * that can exist contributes no rectangle, the reader's rule for every other foreign attribute. */
25
16
  export declare function decodeSqrefRects(sqref: string): MergeRect[];
17
+ /**
18
+ * Re-anchor an OOXML `sqref` through a splice of `count` lines at `start` on `axis`, the inverse of
19
+ * {@link decodeSqrefRects}. Returns `undefined` when the splice deleted every area the `sqref` named:
20
+ * an empty `sqref` is not writable, so the entry holding it goes too.
21
+ *
22
+ * Each space-separated area shifts on its own, and one the splice does not move is returned as the
23
+ * *original text*. That matters for a file the library did not author: `B:B` and `B1:B1048576` decode
24
+ * identically, so a re-encode would rewrite a foreign spelling and cost the byte-clean round trip.
25
+ */
26
+ export declare function shiftSqref(sqref: string, axis: 'row' | 'col', start: number, count: number, delta: number): string | undefined;
@@ -1,12 +1,5 @@
1
- import { decodeRange } from './address.js';
2
- export function masterOf(rects, row, col) {
3
- for (const rect of rects) {
4
- if (row >= rect.top && row <= rect.bottom && col >= rect.left && col <= rect.right) {
5
- return { row: rect.top, col: rect.left };
6
- }
7
- }
8
- return { row, col };
9
- }
1
+ import { encodeCornerRef, tryDecodeRange } from './address.js';
2
+ import { isDeletedSpan, shiftIndex } from './grid-shift.js';
10
3
  export function clearCoveredValues(rows, rect) {
11
4
  for (let row = rect.top; row <= rect.bottom; row++) {
12
5
  const cols = rows.get(row);
@@ -26,7 +19,10 @@ export function decodeSqrefRects(sqref) {
26
19
  for (const part of sqref.split(/\s+/)) {
27
20
  if (part === '')
28
21
  continue;
29
- const { top, left, bottom, right } = decodeRange(part);
22
+ const decoded = tryDecodeRange(part);
23
+ if (decoded === undefined)
24
+ continue;
25
+ const { top, left, bottom, right } = decoded;
30
26
  rects.push({
31
27
  top: top ?? 1,
32
28
  left: left ?? 1,
@@ -36,3 +32,33 @@ export function decodeSqrefRects(sqref) {
36
32
  }
37
33
  return rects;
38
34
  }
35
+ export function shiftSqref(sqref, axis, start, count, delta) {
36
+ const areas = [];
37
+ for (const area of sqref.split(/\s+/)) {
38
+ if (area === '')
39
+ continue;
40
+ const shifted = shiftSqrefArea(area, axis, start, count, delta);
41
+ if (shifted !== undefined)
42
+ areas.push(shifted);
43
+ }
44
+ return areas.length > 0 ? areas.join(' ') : undefined;
45
+ }
46
+ function shiftSqrefArea(area, axis, start, count, delta) {
47
+ const decoded = tryDecodeRange(area);
48
+ if (decoded === undefined)
49
+ return area;
50
+ const { top, left, bottom, right } = decoded;
51
+ const [lo, hi] = axis === 'row' ? [top, bottom] : [left, right];
52
+ if (lo === undefined || hi === undefined)
53
+ return area;
54
+ if (isDeletedSpan(lo, hi, start, count))
55
+ return undefined;
56
+ const movedLo = shiftIndex(lo, start, count, delta, axis);
57
+ const movedHi = shiftIndex(hi, start, count, delta, axis);
58
+ if (movedLo === lo && movedHi === hi)
59
+ return area;
60
+ const [tl, br] = axis === 'row'
61
+ ? [encodeCornerRef(left, movedLo), encodeCornerRef(right, movedHi)]
62
+ : [encodeCornerRef(movedLo, top), encodeCornerRef(movedHi, bottom)];
63
+ return area.includes(':') ? `${tl}:${br}` : tl;
64
+ }