@shbernal/ts-xlsx 1.0.3 → 1.2.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 (136) hide show
  1. package/README.md +21 -0
  2. package/dist/core/address.js +0 -33
  3. package/dist/core/autofilter.js +0 -9
  4. package/dist/core/cell.d.ts +10 -0
  5. package/dist/core/cell.js +4 -123
  6. package/dist/core/color-resolution.js +0 -60
  7. package/dist/core/column.d.ts +9 -1
  8. package/dist/core/column.js +0 -52
  9. package/dist/core/comment-thread.js +0 -45
  10. package/dist/core/conditional-formatting-overlay.js +0 -11
  11. package/dist/core/conditional-formatting.js +0 -10
  12. package/dist/core/containers.js +0 -15
  13. package/dist/core/data-validation-overlay.js +0 -25
  14. package/dist/core/data-validation.js +0 -7
  15. package/dist/core/date.js +0 -39
  16. package/dist/core/formula.js +0 -117
  17. package/dist/core/grid-edits.js +0 -38
  18. package/dist/core/image.js +0 -25
  19. package/dist/core/internal.js +0 -27
  20. package/dist/core/limits.d.ts +30 -0
  21. package/dist/core/limits.js +2 -0
  22. package/dist/core/merge.d.ts +18 -0
  23. package/dist/core/merge.js +22 -7
  24. package/dist/core/modern-functions.js +0 -17
  25. package/dist/core/page-setup.js +0 -5
  26. package/dist/core/pivot-table.d.ts +1 -1
  27. package/dist/core/pivot-table.js +0 -44
  28. package/dist/core/preserved.js +0 -4
  29. package/dist/core/protection.js +0 -26
  30. package/dist/core/range.js +0 -112
  31. package/dist/core/row-input.d.ts +24 -0
  32. package/dist/core/row-input.js +29 -0
  33. package/dist/core/row.d.ts +9 -1
  34. package/dist/core/row.js +0 -58
  35. package/dist/core/style.js +0 -26
  36. package/dist/core/table-style.js +0 -28
  37. package/dist/core/table.js +0 -97
  38. package/dist/core/text-metrics.d.ts +20 -0
  39. package/dist/core/text-metrics.js +10 -0
  40. package/dist/core/theme.js +0 -85
  41. package/dist/core/value.d.ts +40 -0
  42. package/dist/core/value.js +30 -48
  43. package/dist/core/workbook-protection.js +0 -13
  44. package/dist/core/workbook.d.ts +13 -0
  45. package/dist/core/workbook.js +11 -368
  46. package/dist/core/worksheet-model.js +2 -23
  47. package/dist/core/worksheet.d.ts +22 -0
  48. package/dist/core/worksheet.js +14 -485
  49. package/dist/customui/errors.js +0 -10
  50. package/dist/customui/index.js +0 -5
  51. package/dist/customui/ribbon.js +0 -43
  52. package/dist/entries/core.d.ts +3 -1
  53. package/dist/entries/core.js +3 -7
  54. package/dist/entries/csv.js +0 -4
  55. package/dist/entries/customui.js +0 -2
  56. package/dist/entries/errors.js +0 -13
  57. package/dist/entries/vba.js +0 -5
  58. package/dist/entries/xlsb.js +0 -6
  59. package/dist/entries/xlsx.js +0 -13
  60. package/dist/errors.d.ts +14 -0
  61. package/dist/errors.js +5 -56
  62. package/dist/index.js +0 -12
  63. package/dist/io/csv/read.js +0 -18
  64. package/dist/io/csv/write.js +5 -35
  65. package/dist/io/opc/errors.js +0 -30
  66. package/dist/io/opc/inflate.js +0 -35
  67. package/dist/io/opc/namespaces.js +0 -15
  68. package/dist/io/opc/part-paths.js +0 -11
  69. package/dist/io/opc/read-opc.js +0 -31
  70. package/dist/io/opc/read-options.js +0 -6
  71. package/dist/io/opc/rels.js +0 -13
  72. package/dist/io/opc/sniff-format.js +0 -50
  73. package/dist/io/style/xf-style.js +0 -31
  74. package/dist/io/xlsb/errors.js +0 -10
  75. package/dist/io/xlsb/formula.js +1 -99
  76. package/dist/io/xlsb/primitives.js +1 -83
  77. package/dist/io/xlsb/ptg-functions.js +0 -34
  78. package/dist/io/xlsb/read-shared-strings.js +0 -7
  79. package/dist/io/xlsb/read-styles.js +3 -66
  80. package/dist/io/xlsb/read-worksheet.js +4 -87
  81. package/dist/io/xlsb/read.js +2 -60
  82. package/dist/io/xlsb/record-stream.js +0 -28
  83. package/dist/io/xlsb/record-types.js +0 -19
  84. package/dist/io/xlsx/cell-accumulator.js +0 -32
  85. package/dist/io/xlsx/cell-value.js +0 -29
  86. package/dist/io/xlsx/color-xml.d.ts +6 -0
  87. package/dist/io/xlsx/color-xml.js +42 -0
  88. package/dist/io/xlsx/comments.js +0 -84
  89. package/dist/io/xlsx/conditional-formatting.d.ts +1 -1
  90. package/dist/io/xlsx/conditional-formatting.js +1 -97
  91. package/dist/io/xlsx/data-validation.js +0 -56
  92. package/dist/io/xlsx/edit-vba.js +0 -54
  93. package/dist/io/xlsx/errors.js +0 -12
  94. package/dist/io/xlsx/hyperlinks.js +0 -40
  95. package/dist/io/xlsx/images.js +0 -32
  96. package/dist/io/xlsx/namespaces.js +0 -42
  97. package/dist/io/xlsx/package-plan.js +0 -47
  98. package/dist/io/xlsx/pivot.js +0 -22
  99. package/dist/io/xlsx/{pivot-read.js → read-pivot.js} +0 -30
  100. package/dist/io/xlsx/read-rows.js +1 -83
  101. package/dist/io/xlsx/{shared-strings-read.js → read-shared-strings.js} +0 -11
  102. package/dist/io/xlsx/read-styles.d.ts +25 -1
  103. package/dist/io/xlsx/read-styles.js +61 -108
  104. package/dist/io/xlsx/read-worksheet.js +1 -88
  105. package/dist/io/xlsx/read.js +3 -211
  106. package/dist/io/xlsx/relationships.js +0 -11
  107. package/dist/io/xlsx/rich-runs.js +0 -16
  108. package/dist/io/xlsx/rich-text.js +0 -12
  109. package/dist/io/xlsx/shared-formulas.js +0 -7
  110. package/dist/io/xlsx/shared-strings.js +0 -21
  111. package/dist/io/xlsx/sheet-properties.js +1 -64
  112. package/dist/io/xlsx/styles.d.ts +1 -29
  113. package/dist/io/xlsx/styles.js +2 -366
  114. package/dist/io/xlsx/tables.js +5 -52
  115. package/dist/io/xlsx/threaded-comments.js +0 -103
  116. package/dist/io/xlsx/workbook-xml.js +0 -94
  117. package/dist/io/xlsx/worksheet-xml.js +0 -174
  118. package/dist/io/xlsx/write-stream.js +0 -137
  119. package/dist/io/xlsx/write.js +1 -134
  120. package/dist/io/xlsx/x14-ext.js +0 -8
  121. package/dist/vba/cfb-writer.js +12 -59
  122. package/dist/vba/cfb.js +2 -32
  123. package/dist/vba/codepage.js +1 -23
  124. package/dist/vba/errors.js +0 -15
  125. package/dist/vba/index.js +0 -10
  126. package/dist/vba/ms-ovba.js +3 -46
  127. package/dist/vba/project-editor.js +4 -89
  128. package/dist/vba/project.js +2 -34
  129. package/dist/vba/vba-encoding.js +1 -17
  130. package/dist/xml/errors.js +0 -10
  131. package/dist/xml/xml-read.js +3 -82
  132. package/dist/xml/xml.js +0 -39
  133. package/package.json +9 -10
  134. package/skills/ts-xlsx-upstream/SKILL.md +178 -0
  135. /package/dist/io/xlsx/{pivot-read.d.ts → read-pivot.d.ts} +0 -0
  136. /package/dist/io/xlsx/{shared-strings-read.d.ts → read-shared-strings.d.ts} +0 -0
@@ -1,7 +1,3 @@
1
- // The worksheet-body reader: one `xl/worksheets/sheetN.xml` in, its cells/columns/rows/print-setup
2
- // applied onto a {@link Worksheet}. It is a single streaming pass driving a handful of accumulators
3
- // (the cell being read, shared-formula masters, an autofilter draft, the current page-break axis) so
4
- // each element commits its state as it closes. Style indices resolve through the parsed style table.
5
1
  import { decodeRange } from '../../core/address.js';
6
2
  import { isCustomFilterOperator, } from '../../core/autofilter.js';
7
3
  import { INTERNAL } from '../../core/internal.js';
@@ -9,15 +5,9 @@ import { SHEET_PROTECTION_FLAGS, } from '../../core/protection.js';
9
5
  import { assignStyleFacets } from '../../core/style.js';
10
6
  import { boolPresent, boolStrict, boolTristate, localName, parseXml, } from '../../xml/xml-read.js';
11
7
  import { CellAccumulator } from './cell-accumulator.js';
12
- import { parseColor } from './styles.js';
8
+ import { parseColor } from './color-xml.js';
13
9
  const MARGIN_SIDES = ['left', 'right', 'top', 'bottom', 'header', 'footer'];
14
- // Worksheet elements that commit on their close: a formatted-but-empty `<c/>` and a criteria-free
15
- // self-closing `<autoFilter/>` are expanded to open+close so each finalises once in onClose. The
16
- // text-bearing `<f/>`/`<v/>`/`<t/>` are deliberately excluded — an empty one must not commit.
17
10
  const WORKSHEET_EMPTY_CLOSES = new Set(['c', 'autoFilter']);
18
- // Fold a filter column's accumulated `<filters>` or `<customFilters>` state into one criteria value,
19
- // or null when it carried nothing filterable. A `<filters>` block with no values and no blank flag,
20
- // or a `<customFilters>` with no predicates, is a no-op that would round-trip as noise, so it drops.
21
11
  function pendingFilterCriteria(values, blank, predicates, and) {
22
12
  if (values !== null && (values.length > 0 || blank)) {
23
13
  return { kind: 'values', values, blank };
@@ -27,12 +17,6 @@ function pendingFilterCriteria(values, blank, predicates, and) {
27
17
  }
28
18
  return null;
29
19
  }
30
- // Read a <sheetProtection> element back into a SheetProtection — the deserialization mirror of the
31
- // writer. `sheet="0"` (or "false") means the element records an *un*protected sheet, so nothing is
32
- // restored. Each flag attribute is the INVERSE of the author's allow-flag ("1" forbids, "0" permits),
33
- // and only attributes actually present are carried, so an omitted (default-valued) flag stays absent —
34
- // exactly what the writer emitted. A password credential is preserved verbatim in its agile form
35
- // (algorithm, hash, salt, spin count); there is no plaintext password to recover, so it is not re-hashed.
36
20
  function parseSheetProtection(attrs) {
37
21
  if (attrs.sheet === '0' || attrs.sheet === 'false')
38
22
  return undefined;
@@ -60,9 +44,6 @@ function parseSheetProtection(attrs) {
60
44
  }
61
45
  return { flags };
62
46
  }
63
- // Autofilter accumulation. The sheet `<autoFilter ref>` seeds a draft; each `<filterColumn colId>`
64
- // opens a column whose criteria (`<filters>` values or `<customFilters>` predicates) stream into the
65
- // draft until `</filterColumn>`, and `</autoFilter>` commits the whole thing to the sheet.
66
47
  class AutoFilterAccumulator {
67
48
  #ref = null;
68
49
  #columns = [];
@@ -71,14 +52,10 @@ class AutoFilterAccumulator {
71
52
  #blank = false;
72
53
  #predicates = null;
73
54
  #and = false;
74
- // Seed a draft from the range. (A table's own `<autoFilter>` also matches, but table sheets route
75
- // through parseTable, so this only ever sees the sheet-level one.)
76
55
  begin(attrs) {
77
56
  this.#ref = attrs.ref !== undefined && attrs.ref !== '' ? attrs.ref : null;
78
57
  this.#columns = [];
79
58
  }
80
- // Open a criteria block for one column, offset `colId` from the range's left edge. Reset the
81
- // per-column accumulators; whichever child (`<filters>`/`<customFilters>`) opens fills one.
82
59
  beginColumn(attrs) {
83
60
  this.#colId = attrs.colId !== undefined ? Number(attrs.colId) : -1;
84
61
  this.#values = null;
@@ -88,8 +65,6 @@ class AutoFilterAccumulator {
88
65
  }
89
66
  beginValues(attrs) {
90
67
  this.#values = [];
91
- // `blank` defaults off when absent, so presence is required first — then it reads as an
92
- // on-when-present flag. (`boolPresent` alone would treat the absent attribute as on.)
93
68
  this.#blank = attrs.blank !== undefined && boolPresent(attrs.blank);
94
69
  }
95
70
  addValue(attrs) {
@@ -100,8 +75,6 @@ class AutoFilterAccumulator {
100
75
  this.#predicates = [];
101
76
  this.#and = attrs.and !== undefined && boolPresent(attrs.and);
102
77
  }
103
- // The operator attribute defaults to `equal` when absent (per CT_CustomFilter); an operand is
104
- // likewise optional. An unrecognised operator drops the predicate rather than guessing.
105
78
  addCustom(attrs) {
106
79
  if (this.#predicates === null)
107
80
  return;
@@ -110,18 +83,12 @@ class AutoFilterAccumulator {
110
83
  this.#predicates.push({ operator, val: attrs.val ?? '' });
111
84
  }
112
85
  }
113
- // Assemble this column's criteria from whichever accumulator filled. A column whose colId is
114
- // negative, or whose criteria are empty (no values, no blank, no predicates), carries nothing
115
- // filterable and is dropped so a re-write stays clean — load-repair, not authoring.
116
86
  endColumn() {
117
87
  const criteria = pendingFilterCriteria(this.#values, this.#blank, this.#predicates, this.#and);
118
88
  if (this.#colId >= 0 && criteria !== null) {
119
89
  this.#columns.push({ colId: this.#colId, criteria });
120
90
  }
121
91
  }
122
- // Commit the accumulated autofilter to the sheet. Runs on `</autoFilter>` — including the synthesized
123
- // close of a criteria-free self-closing `<autoFilter/>`. Columns whose colId falls outside the range
124
- // are dropped here so the strict setter never trips on hostile input.
125
92
  commit(sheet) {
126
93
  if (this.#ref === null)
127
94
  return;
@@ -131,16 +98,11 @@ class AutoFilterAccumulator {
131
98
  sheet.autoFilter = { ref: this.#ref, columns: this.#columns.filter((c) => c.colId < width) };
132
99
  }
133
100
  catch {
134
- // unbounded or malformed autofilter range in the source file — ignore it
135
101
  }
136
102
  this.#ref = null;
137
103
  this.#columns = [];
138
104
  }
139
105
  }
140
- // Page-break accumulation. `<brk>` elements appear under both `<rowBreaks>` and `<colBreaks>`; a break
141
- // container's open points the accumulator at that axis's list (null outside any container), so a
142
- // `<brk>` lands on the right axis, and the matching close clears it. A self-closing
143
- // `<rowBreaks/>`/`<colBreaks/>` fires no close, so a new open simply reassigns the target.
144
106
  class PageBreakAccumulator {
145
107
  #target = null;
146
108
  begin(target) {
@@ -149,8 +111,6 @@ class PageBreakAccumulator {
149
111
  end() {
150
112
  this.#target = null;
151
113
  }
152
- // `id` is the row/column the layout splits before; a non-positive or non-integer id is hostile input
153
- // and dropped rather than trusted. A `<brk>` outside any break container has no axis and is ignored.
154
114
  add(attrs) {
155
115
  if (this.#target === null)
156
116
  return;
@@ -167,24 +127,15 @@ class PageBreakAccumulator {
167
127
  }
168
128
  }
169
129
  export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
170
- // The one `<c>` currently being read: its address/type/style, formula, value, inline text, rich
171
- // runs, and the sheet-spanning shared-formula master map. Each `<c>` resets it and commits it.
172
130
  const cell = new CellAccumulator();
173
131
  let inInlineString = false;
174
132
  let capture = false;
175
133
  let text = '';
176
- // A row with customFormat="1" supplies a default style for its cells that carry no `s`.
177
134
  let rowStyle = -1;
178
135
  let rowCustomFormat = false;
179
136
  const autoFilter = new AutoFilterAccumulator();
180
137
  const pageBreaks = new PageBreakAccumulator();
181
- // A column's `style` is the default for its cells that carry no style of their own; this
182
- // maps a column index to that style index so a bare cell can inherit it (as Excel does,
183
- // without stamping every cell). Columns are parsed before any cell references them.
184
138
  const columnStyle = new Map();
185
- // Commit the cell held in the accumulator, resolving its style from its own `s`, then its row's
186
- // (when customFormat), then its column's default — the order Excel applies. Runs on `</c>` close,
187
- // including the synthesized close of a self-closing `<c/>` formatted-but-empty cell.
188
139
  const finalizeCellFromState = () => {
189
140
  const styleIndex = cell.styleIndex >= 0
190
141
  ? cell.styleIndex
@@ -216,12 +167,10 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
216
167
  cell.beginInlineString();
217
168
  break;
218
169
  case 'r':
219
- // A run inside a rich inline string. Its `<rPr>` (if any) and `<t>` follow.
220
170
  if (inInlineString)
221
171
  cell.runs.beginRun();
222
172
  break;
223
173
  case 'rPr':
224
- // The run's formatting bundle; its self-closing children stream into the default branch.
225
174
  cell.runs.beginProperties();
226
175
  break;
227
176
  case 'f':
@@ -238,21 +187,14 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
238
187
  case 'evenFooter':
239
188
  case 'firstHeader':
240
189
  case 'firstFooter':
241
- // A `<headerFooter>` child carries its header/footer definition as text (the `&`-prefixed
242
- // section/format tokens, e.g. `&C&"Arial"&G`). Capture it verbatim so a round-trip preserves
243
- // a header image's `&G` picture token and every other formatting directive.
244
190
  capture = true;
245
191
  break;
246
192
  case 'mergeCell':
247
- // A well-formed file never declares overlapping merges; a corrupt one might. Reject the
248
- // bad range at the model boundary, but don't let one abort the whole parse — drop it and
249
- // keep reading the valid geometry.
250
193
  if (attrs.ref !== undefined && attrs.ref !== '') {
251
194
  try {
252
195
  sheet.mergeCells(attrs.ref);
253
196
  }
254
197
  catch {
255
- // overlapping/malformed merge in the source file — skip it
256
198
  }
257
199
  }
258
200
  break;
@@ -299,8 +241,6 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
299
241
  autoFilter.addCustom(attrs);
300
242
  break;
301
243
  default:
302
- // A run's `<rPr>` child (`<b/>`, `<sz>`, `<color>`, `<rFont>`, …) sets one font facet; it
303
- // is self-closing, so it is read here on open. Nothing else uses the default branch.
304
244
  cell.runs.applyProperty(local, attrs);
305
245
  break;
306
246
  }
@@ -321,8 +261,6 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
321
261
  cell.setValue(text);
322
262
  break;
323
263
  case 't':
324
- // A `<t>` inside a run is that run's text; a bare `<t>` directly in the `<is>` is a plain
325
- // inline string. A run takes precedence — a run is also inside the inline string.
326
264
  cell.appendText(text, inInlineString);
327
265
  break;
328
266
  case 'r':
@@ -363,27 +301,18 @@ export function parseWorksheet(xml, sheet, sharedStrings, xfStyles) {
363
301
  },
364
302
  }, { closeEmptyElements: WORKSHEET_EMPTY_CLOSES });
365
303
  }
366
- // Apply one `<sheetPr>` / `<sheetView>` / print-setup child to the sheet. These are the worksheet's
367
- // layout and print metadata; grouping them here keeps the cell-reading switch a pure dispatch. Each
368
- // records only what the source carried, so a file missing a facet leaves it unset and a re-write
369
- // stays byte-clean. Every element here is self-closing (its state is all in attributes), so it is
370
- // read on open.
371
304
  function applySheetProperties(local, attrs, sheet) {
372
305
  switch (local) {
373
306
  case 'tabColor':
374
- // A `<sheetPr>` child.
375
307
  sheet.tabColor = parseColor(attrs);
376
308
  break;
377
309
  case 'outlinePr':
378
- // A `<sheetPr>` child.
379
310
  if (attrs.summaryBelow !== undefined)
380
311
  sheet.outline.summaryBelow = boolPresent(attrs.summaryBelow);
381
312
  if (attrs.summaryRight !== undefined)
382
313
  sheet.outline.summaryRight = boolPresent(attrs.summaryRight);
383
314
  break;
384
315
  case 'pane':
385
- // A `<sheetView>` child recording a frozen (or split) pane. Only a frozen pane maps onto the
386
- // model's view; a source without one leaves `view` empty, so a re-write emits no pane.
387
316
  if (attrs.state === 'frozen' || attrs.state === 'frozenSplit') {
388
317
  sheet.view.state = 'frozen';
389
318
  if (attrs.xSplit !== undefined)
@@ -395,8 +324,6 @@ function applySheetProperties(local, attrs, sheet) {
395
324
  }
396
325
  break;
397
326
  case 'pageSetUpPr':
398
- // The fit-to-page flag, a `<sheetPr>` child. Recorded only when the attribute is present, so a
399
- // `<pageSetUpPr>` present for other reasons (e.g. `autoPageBreaks`) leaves `fitToPage` unset.
400
327
  if (attrs.fitToPage !== undefined)
401
328
  sheet.pageSetup.fitToPage = boolPresent(attrs.fitToPage);
402
329
  break;
@@ -419,8 +346,6 @@ function applyColumn(sheet, attrs, xfStyles, columnStyle) {
419
346
  const width = attrs.width !== undefined ? Number(attrs.width) : undefined;
420
347
  const hidden = boolStrict(attrs.hidden);
421
348
  const styleIndex = attrs.style !== undefined ? Number(attrs.style) : -1;
422
- // The column's style resolves to the same facet bundle a cell's does; mirror all of it onto the
423
- // column model so `getColumn(i)` reflects the declared default, not just its number format.
424
349
  const style = styleIndex >= 0 ? xfStyles[styleIndex] : undefined;
425
350
  for (let index = min; index <= max; index++) {
426
351
  const column = sheet.getColumn(index);
@@ -437,7 +362,6 @@ function applyColumn(sheet, attrs, xfStyles, columnStyle) {
437
362
  column.collapsed = true;
438
363
  if (style !== undefined)
439
364
  assignStyleFacets(column, style);
440
- // Record the column's style so a bare cell in it can inherit the full column format on read.
441
365
  if (styleIndex >= 0)
442
366
  columnStyle.set(index, styleIndex);
443
367
  }
@@ -446,10 +370,6 @@ function applyRow(sheet, attrs) {
446
370
  const number = Number(attrs.r);
447
371
  if (!Number.isInteger(number) || number < 1)
448
372
  return;
449
- // A `<row>` that states no attribute at all leaves no format record behind — the handle creates
450
- // one only when something is written through it. That is the right reading: a bare `<row r="5"/>`
451
- // carries no formatting to round-trip, and fabricating an empty record for it would put row 5 in
452
- // the used range on the strength of an element that says nothing.
453
373
  const row = sheet.getRow(number);
454
374
  if (attrs.ht !== undefined && attrs.customHeight !== '0') {
455
375
  const height = Number(attrs.ht);
@@ -466,9 +386,6 @@ function applyRow(sheet, attrs) {
466
386
  if (boolStrict(attrs.collapsed))
467
387
  row.collapsed = true;
468
388
  }
469
- // Read the `<printOptions>` boolean toggles back onto the model, storing only the ones the source
470
- // carried so a re-write stays byte-clean. An OOXML boolean is `1`/`true` for on and `0`/`false` for
471
- // off; a present-but-unrecognised token is dropped rather than coerced.
472
389
  function applyPrintOptions(printOptions, attrs) {
473
390
  const horizontalCentered = boolTristate(attrs.horizontalCentered);
474
391
  if (horizontalCentered !== undefined)
@@ -496,10 +413,6 @@ function applyMargins(margins, attrs) {
496
413
  margins[side] = value;
497
414
  }
498
415
  }
499
- // Read the `<pageSetup>` print-scaling attributes back onto the model, setting only those the
500
- // source carried so a re-write stays byte-clean. Numeric attributes that fail to parse are
501
- // dropped rather than stored as NaN; the enumerated ones are trusted verbatim (an unexpected token
502
- // round-trips harmlessly as an unknown string).
503
416
  function applyPageSetup(pageSetup, attrs) {
504
417
  const num = (raw) => {
505
418
  if (raw === undefined)