werkmap 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,10 +6,10 @@ Writing is the whole surface, so the package stays small enough to audit. The co
6
6
 
7
7
  - **Byte-identical output.** Nothing consults a clock, a locale or a random source, and the ZIP epoch is pinned, so two renders of the same report are the same bytes and a build that caches by content hash keeps working.
8
8
  - **Foreign readers accept it.** The suite loads every workbook it writes back through ExcelJS, an independent implementation — so the tests prove a real reader opens the file, not just that the bytes look plausible.
9
- - **Zero dependencies.** 7.1 kB minified and brotlied, for the whole writer.
9
+ - **Zero dependencies.** 7.7 kB minified and brotlied, for the whole writer.
10
10
  - **Strict CSP, including in the browser.** No `unsafe-eval` and no `unsafe-inline`. A Chromium page under `default-src 'none'; script-src 'self'` writes a workbook and reports any violation back, and the Node suite runs on `--disallow-code-generation-from-strings`.
11
11
  - **Write-only, deliberately.** No reader, no formula engine, no chart support — see [Is werkmap the right tool?](#is-werkmap-the-right-tool) before you install it.
12
- - **Hardened.** 72 tests at 100% branch coverage.
12
+ - **Hardened.** 85 tests at 100% branch coverage.
13
13
 
14
14
  ```js
15
15
  import { workbook } from "werkmap";
@@ -118,7 +118,17 @@ The finished package. This does not seal the document: call it as often as you l
118
118
 
119
119
  `cells` is an array; the returned number is the row's 1-based position, which `merge`, `freeze` and `place` take.
120
120
 
121
- `options.level` is the row's **outline level**, an integer from 0 to 7. A reader draws the levels as collapsible groups in its left margin — the way Excel's own Group command does — and reads the summary row as the one **below** each group, which is where a total row sits. `0`, the default, is a row outside any group, and a row given no options is at 0. Nothing is hidden or collapsed: every row you wrote is in the document and open, and what the reader does with the controls is theirs.
121
+ `options.level` is the row's **outline level**, an integer from 0 to 7. A reader draws the levels as collapsible groups in its left margin — the way Excel's own Group command does — and reads the summary row as the one **below** each group, which is where a total row sits. `0`, the default, is a row outside any group, and a row given no options is at 0.
122
+
123
+ `options.hidden` and `options.collapsed` say how a group opens. A collapsed group is **both**: its content rows `hidden`, and its summary row `collapsed`. Hiding alone leaves the group's control showing expanded over rows nobody can see, which is a document in two minds rather than a collapsed group. Neither is set by default, so a row you write is in the document and open unless you say otherwise, and what the reader then does with the controls is theirs.
124
+
125
+ ```js
126
+ sheet.row([{ value: "North" }], { level: 1 });
127
+ sheet.row([{ value: "Laptop" }], { level: 2, hidden: true });
128
+ sheet.row([{ value: "Subtotal" }], { level: 1, collapsed: true });
129
+ ```
130
+
131
+ An option this does not know is **refused**, not ignored: a misspelt one would otherwise be a row attribute you asked for and never got.
122
132
 
123
133
  A sheet that outlines any row also states the workbook's default row height, 15 points, because the format requires it beside the deepest level. A sheet with no outline states neither, and a reader keeps its own default. That is the one place this writer names a row height, and it names the default rather than one of its own.
124
134
 
@@ -167,12 +177,22 @@ Excel also records an autofilter as a sheet-scoped `_xlnm._FilterDatabase` defin
167
177
 
168
178
  ### `sheet.print(setup)`
169
179
 
170
- How this worksheet prints: `{ margin, size, orientation, fit, titles }`, every key optional. `margin` is all four page margins in points, `size` one of `letter`, `tabloid`, `legal`, `A3`, `A4`, `A5`, `orientation` either `portrait` or `landscape`, and `fit: true` scales the sheet to one page wide and as many pages tall as it takes.
180
+ How this worksheet prints: `{ margin, size, orientation, fit, titles, header, footer }`, every key optional. `margin` is all four page margins in points, `size` one of `letter`, `tabloid`, `legal`, `A3`, `A4`, `A5`, `orientation` either `portrait` or `landscape`, and `fit: true` scales the sheet to one page wide and as many pages tall as it takes.
171
181
 
172
182
  A worksheet that never calls this carries **no print setup at all**, so a reader applies its own defaults rather than this writer's opinion. Calls merge, so two calls naming different keys both take effect. Print setup is per worksheet, which is where OOXML puts it.
173
183
 
174
184
  `titles: n` repeats the top `n` rows at the top of every printed page — the paper counterpart of `freeze`, which keeps them in view on screen. `0` clears it, as it does for a freeze. This one is written as a `_xlnm.Print_Titles` defined name in `xl/workbook.xml` rather than in the sheet part, and it is scoped to this worksheet alone, so each sheet of a workbook repeats its own rows.
175
185
 
186
+ `header` and `footer` are text printed at the top and the bottom of every page. Either is one **section**, printed on the left, or the three sections the format has — `{ left, center, right }`, each optional. A section is a string, or an array of **parts** in order:
187
+
188
+ - a string, printed as it is;
189
+ - `{ field: "page" }` or `{ field: "pages" }`, the page number and the page count, which the reader fills in as it paginates — `["Page ", { field: "page" }, " of ", { field: "pages" }]`;
190
+ - `{ text, bold, size }`, text in a look: `bold: true` and a font size from 1 to 99, each holding until a later part changes it, and starting plain at every section.
191
+
192
+ `firstHeader` and `firstFooter` are the first page's, where it differs from the rest. Naming either makes the first page different, and a first-page part you do not name prints nothing on that page — so a footer that should print everywhere goes in `footer`, and only what changes on page one goes in `firstFooter`.
193
+
194
+ **Text is text**: the format has a small language of `&`-codes for dates, file names and more, and this surface exposes none of it, so an ampersand in your text prints as an ampersand and a newline prints as a line break. A reader holds at most **255 characters** per header or footer as stored — the section codes, each field's code, each look's code and the doubled ampersands count — and a longer text throws rather than being cut mid-sentence by the reader. Nothing appears on screen; a header is print furniture, and the grid is unchanged.
195
+
176
196
  ### `sheet.place(id, { row, col, width, height })`
177
197
 
178
198
  One floating picture anchored to the top-left of a 1-based cell, drawn at `width` × `height` CSS pixels at 96 dpi.
package/lib/index.d.ts CHANGED
@@ -76,6 +76,33 @@ export interface Placement {
76
76
  }
77
77
 
78
78
  /** How a worksheet prints. Every key is optional. */
79
+ /**
80
+ * Text for a page header or footer: a string, or parts in order, where a
81
+ * `{ field }` is a value the reader fills in as it paginates — `page` is the
82
+ * page number and `pages` the page count.
83
+ */
84
+ /**
85
+ * One part of a page header or footer: text, a `{ field }` the reader fills
86
+ * in as it paginates (`page` is the page number, `pages` the page count), or
87
+ * `{ text }` in a look — bold, and a font size from 1 to 99 — that holds
88
+ * until a later part changes it.
89
+ */
90
+ export type PrintPart =
91
+ | string
92
+ | { field: "page" | "pages" }
93
+ | { text: string; bold?: boolean; size?: number };
94
+
95
+ /** One section of a header: a string, or parts in order. */
96
+ export type PrintSection = string | readonly PrintPart[];
97
+
98
+ /**
99
+ * Text for a page header or footer: one section, printed on the left, or the
100
+ * three sections the format has, each optional.
101
+ */
102
+ export type PrintText =
103
+ | PrintSection
104
+ | { left?: PrintSection; center?: PrintSection; right?: PrintSection };
105
+
79
106
  export interface PrintSetup {
80
107
  /** All four page margins, in points. Defaults to the reader's own. */
81
108
  margin?: number;
@@ -89,6 +116,24 @@ export interface PrintSetup {
89
116
  * the way a `freeze` of `0` does. Scoped to this worksheet alone.
90
117
  */
91
118
  titles?: number;
119
+ /**
120
+ * Text printed at the top of every page: one section on the left, or the
121
+ * three sections named. Text is text: an ampersand is an ampersand and a
122
+ * newline a line break. The format's own codes are not on this surface;
123
+ * the page number, the page count, bold and a size are named as parts
124
+ * instead. At most 255 characters as stored.
125
+ */
126
+ header?: PrintText;
127
+ /** Text printed at the bottom of every page. Same rules as `header`. */
128
+ footer?: PrintText;
129
+ /**
130
+ * The first page's header, where it differs from the rest. Naming either
131
+ * first-page part makes the first page different; a part not named prints
132
+ * nothing there.
133
+ */
134
+ firstHeader?: PrintText;
135
+ /** The first page's footer, where it differs from the rest. */
136
+ firstFooter?: PrintText;
92
137
  }
93
138
 
94
139
  /** The rectangle an autofilter covers. 1-based, and inclusive on all four sides. */
@@ -107,6 +152,17 @@ export interface RowOptions {
107
152
  * the one below the group. `0`, the default, is a row outside any group.
108
153
  */
109
154
  level?: number;
155
+ /**
156
+ * Whether the row is hidden. A collapsed outline group is its content rows
157
+ * hidden and its summary row `collapsed`; hiding alone leaves the group's
158
+ * control showing expanded over rows nobody can see.
159
+ */
160
+ hidden?: boolean;
161
+ /**
162
+ * Whether this row carries the control of a collapsed group. It goes on the
163
+ * **summary** row — the one below the group — not on the hidden rows.
164
+ */
165
+ collapsed?: boolean;
110
166
  }
111
167
 
112
168
  export interface Sheet {
@@ -115,7 +171,9 @@ export interface Sheet {
115
171
  /**
116
172
  * Append a row. `null` is an empty unstyled cell. Returns the row's 1-based
117
173
  * position, which `merge` and `place` take. `options.level` puts the row at
118
- * an outline level.
174
+ * an outline level, and `hidden` / `collapsed` say whether it is shown and
175
+ * whether it carries a collapsed group's control. An option this does not
176
+ * know is refused rather than ignored.
119
177
  */
120
178
  row(cells: readonly (Cell | null)[], options?: RowOptions): number;
121
179
  /**
package/lib/index.js CHANGED
@@ -677,17 +677,184 @@ const POINTS_PER_INCH = 72;
677
677
  // `pageMargins` has no optional attributes, so a value is owed either way.
678
678
  const FURNITURE = 0.3;
679
679
 
680
+ // Excel's ceiling for a header or a footer, counted on the string it stores:
681
+ // the section code and the doubled ampersands included.
682
+ const MAX_FURNITURE = 255;
683
+
684
+ /**
685
+ * @typedef {string | { field: string } | { text: string, bold?: boolean, size?: number }} PrintPart
686
+ * @typedef {string | ReadonlyArray<PrintPart>} PrintSection
687
+ * @typedef {PrintSection | { left?: PrintSection, center?: PrintSection, right?: PrintSection }} PrintText
688
+ * @typedef {{ margin?: number, size?: string, orientation?: string, fit?: boolean,
689
+ * titles?: number, header?: PrintText, footer?: PrintText,
690
+ * firstHeader?: PrintText, firstFooter?: PrintText }} PrintSetup
691
+ */
692
+
693
+ // The two fields a header may carry, as the format spells them: the page
694
+ // number and the page count, which the reader fills in as it paginates.
695
+ const FIELDS = new Map([
696
+ ["page", "&P"],
697
+ ["pages", "&N"],
698
+ ]);
699
+
700
+ // The three sections of a header, in the order the format reads them.
701
+ const SECTIONS = [
702
+ ["left", "&L"],
703
+ ["center", "&C"],
704
+ ["right", "&R"],
705
+ ];
706
+
707
+ // The four parts of `headerFooter`, in the schema's order, and the setup key
708
+ // each is written from. `differentFirst` rides along once either first-page
709
+ // part is present.
710
+ const FURNITURE_PARTS = [
711
+ ["header", "oddHeader"],
712
+ ["footer", "oddFooter"],
713
+ ["firstHeader", "firstHeader"],
714
+ ["firstFooter", "firstFooter"],
715
+ ];
716
+
717
+ // A header font size is written as two digits, so the format reads exactly
718
+ // two and the text that follows is never mistaken for more of the number.
719
+ const MAX_HEADER_SIZE = 99;
720
+
680
721
  /**
681
- * `pageMargins` and `pageSetup`, or nothing at all. `pageMargins` has no
682
- * optional attributes, so any margin means writing all six; the two this
683
- * surface does not take keep Excel's own header and footer gap.
722
+ * `pageMargins`, `pageSetup` and `headerFooter`, or nothing at all.
723
+ * `pageMargins` has no optional attributes, so any margin means writing all
724
+ * six; the two this surface does not take keep Excel's own header and footer
725
+ * gap.
684
726
  *
685
- * @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number } | null} setup
727
+ * @param {PrintSetup | null} setup
686
728
  */
687
729
  const printXml = (setup) => {
688
730
  if (setup === null) return "";
689
731
  const attributes = setupAttributes(setup);
690
- return marginsXml(setup.margin) + (attributes === "" ? "" : `<pageSetup${attributes}/>`);
732
+ return (
733
+ marginsXml(setup.margin) +
734
+ (attributes === "" ? "" : `<pageSetup${attributes}/>`) +
735
+ furnitureXml(setup)
736
+ );
737
+ };
738
+
739
+ /** Text as the format stores it: an ampersand doubled so none reads as a code.
740
+ * @param {string} text */
741
+ const plain = (text) => text.replace(/&/g, "&&");
742
+
743
+ /**
744
+ * A field as its code.
745
+ * @param {unknown} field
746
+ * @param {string} at
747
+ */
748
+ const fieldCode = (field, at) => {
749
+ const code = FIELDS.get(/** @type {string} */ (field));
750
+ if (code === undefined)
751
+ throw RangeError(`${at}: expected a field of page or pages, got ${JSON.stringify(field)}`);
752
+ return code;
753
+ };
754
+
755
+ /**
756
+ * @typedef {{ bold: boolean, size: number | undefined }} Look
757
+ * What the section's text currently reads as. A code toggles bold and sets a
758
+ * size until the next code, so a part writes one only where its own look
759
+ * differs from what stands.
760
+ */
761
+
762
+ /**
763
+ * @param {unknown} size
764
+ * @param {string} at
765
+ */
766
+ const requireHeaderSize = (size, at) => {
767
+ if (!isIndex(size, MAX_HEADER_SIZE))
768
+ throw RangeError(
769
+ `${at}.size: expected an integer between 1 and ${MAX_HEADER_SIZE}, got ${JSON.stringify(size)}`,
770
+ );
771
+ return String(size).padStart(2, "0");
772
+ };
773
+
774
+ /**
775
+ * The bold code a part owes: `&B` toggles, so one is written only where the
776
+ * part's weight differs from what stands.
777
+ * @param {Record<string, unknown>} part
778
+ * @param {Look} look
779
+ */
780
+ const boldCode = (part, look) => {
781
+ const bold = part.bold === true;
782
+ const toggles = bold !== look.bold;
783
+ look.bold = bold;
784
+ return toggles ? "&B" : "";
785
+ };
786
+
787
+ /**
788
+ * The size code a part owes: a size holds until the next one, so it is
789
+ * written only where the part names one that differs from what stands.
790
+ * @param {Record<string, unknown>} part
791
+ * @param {string} at
792
+ * @param {Look} look
793
+ */
794
+ const sizeCode = (part, at, look) => {
795
+ const size = part.size;
796
+ if (size === undefined || size === look.size) return "";
797
+ look.size = /** @type {number} */ (size);
798
+ return "&" + requireHeaderSize(size, at);
799
+ };
800
+
801
+ /**
802
+ * One part of a section: text, a field as its code, or styled text with the
803
+ * codes that change the look before it.
804
+ * @param {unknown} part
805
+ * @param {string} at
806
+ * @param {Look} look
807
+ */
808
+ const furniturePart = (part, at, look) => {
809
+ if (typeof part === "string") return plain(part);
810
+ const record = requireRecord(part, at, "text, a { field } part or a { text } part");
811
+ if (record.field !== undefined) return fieldCode(record.field, at);
812
+ const codes = boldCode(record, look) + sizeCode(record, at, look);
813
+ return codes + plain(requireString(record.text, `${at}.text`));
814
+ };
815
+
816
+ /**
817
+ * One section as the format stores it. A newline is a line break. The look
818
+ * starts plain at every section, since a section is where a code's reach ends.
819
+ * @param {unknown} text
820
+ * @param {string} at
821
+ */
822
+ const sectionText = (text, at) => {
823
+ const parts = typeof text === "string" ? [text] : text;
824
+ if (!Array.isArray(parts))
825
+ throw TypeError(`${at}: expected text or an array of parts, got ${JSON.stringify(text)}`);
826
+ /** @type {Look} */
827
+ const look = { bold: false, size: undefined };
828
+ return parts.map((part, index) => furniturePart(part, `${at}[${index}]`, look)).join("");
829
+ };
830
+
831
+ /**
832
+ * A header or footer as the format stores it: one section on the left, or
833
+ * the sections a caller named, each behind its code, in the format's order.
834
+ * @param {unknown} text
835
+ * @param {string} at
836
+ */
837
+ const furniture = (text, at) => {
838
+ if (typeof text === "string" || Array.isArray(text)) return "&L" + sectionText(text, at);
839
+ const sections = requireRecord(text, at, "text, parts or { left, center, right } sections");
840
+ return SECTIONS.filter(([key]) => sections[key] !== undefined)
841
+ .map(([key, code]) => code + sectionText(sections[key], `${at}.${key}`))
842
+ .join("");
843
+ };
844
+
845
+ /**
846
+ * `headerFooter`, or nothing for a worksheet that names no part of it. A
847
+ * first-page part makes the first page different, which the element says.
848
+ * @param {PrintSetup} setup
849
+ */
850
+ const furnitureXml = (setup) => {
851
+ const record = /** @type {Record<string, unknown>} */ (setup);
852
+ const written = FURNITURE_PARTS.filter(([key]) => record[key] !== undefined).map(
853
+ ([key, tag]) => `<${tag}>${esc(furniture(record[key], tag))}</${tag}>`,
854
+ );
855
+ if (written.length === 0) return "";
856
+ const first = setup.firstHeader !== undefined || setup.firstFooter !== undefined;
857
+ return `<headerFooter${first ? ' differentFirst="1"' : ""}>${written.join("")}</headerFooter>`;
691
858
  };
692
859
 
693
860
  /** @param {number | undefined} margin in points */
@@ -736,12 +903,29 @@ const MAX_LEVEL = 7;
736
903
  /** @param {unknown} level */
737
904
  const isLevel = (level) => level === 0 || isIndex(level, MAX_LEVEL);
738
905
 
739
- /**
740
- * A row's outline level, or 0 for a row that said nothing.
741
- * @param {unknown} options
742
- */
743
- const requireLevel = (options) => {
744
- const level = requireRecord(options ?? {}, "row: options", "{ level }").level ?? 0;
906
+ // Everything a row may say about itself. Named here because the reader below
907
+ // refuses anything else: a key it quietly dropped would be a caller asking for
908
+ // a row attribute and getting no error and no attribute.
909
+ const ROW_OPTIONS = ["level", "hidden", "collapsed"];
910
+
911
+ /** @param {unknown} value @param {string} where */
912
+ const requireFlag = (value, where) => {
913
+ if (value !== undefined && value !== null && typeof value !== "boolean")
914
+ throw TypeError(`${where}: expected a boolean, got ${JSON.stringify(value)}`);
915
+ return value === true;
916
+ };
917
+
918
+ /** The options object, with nothing in it this writer cannot write. */
919
+ /** @param {unknown} options */
920
+ const requireOptions = (options) => {
921
+ const said = requireRecord(options ?? {}, "row: options", `{ ${ROW_OPTIONS.join(", ")} }`);
922
+ const stray = Object.keys(said).find((name) => !ROW_OPTIONS.includes(name));
923
+ if (stray !== undefined) throw TypeError(`row: options: unknown option ${JSON.stringify(stray)}`);
924
+ return said;
925
+ };
926
+
927
+ /** @param {unknown} level */
928
+ const requireOutline = (level) => {
745
929
  if (!isLevel(level))
746
930
  throw RangeError(
747
931
  `row: level: expected an integer between 0 and ${MAX_LEVEL}, got ${JSON.stringify(level)}`,
@@ -750,11 +934,31 @@ const requireLevel = (options) => {
750
934
  };
751
935
 
752
936
  /**
753
- * A row's opening tag: the level rides along only where there is one.
937
+ * What a row says about itself: its outline level, and whether it is hidden or
938
+ * carries a collapsed group's control.
939
+ * @param {unknown} options
940
+ */
941
+ const requireRow = (options) => {
942
+ const said = requireOptions(options);
943
+ return {
944
+ level: requireOutline(said.level ?? 0),
945
+ hidden: requireFlag(said.hidden, "row: hidden"),
946
+ collapsed: requireFlag(said.collapsed, "row: collapsed"),
947
+ };
948
+ };
949
+
950
+ /**
951
+ * A row's opening tag. The attributes sit in the order `CT_Row` declares them,
952
+ * and each rides along only where it says something.
754
953
  * @param {number} at
755
- * @param {number} level
954
+ * @param {ReturnType<typeof requireRow>} row
756
955
  */
757
- const rowOpen = (at, level) => `<row r="${at}"${level === 0 ? "" : ` outlineLevel="${level}"`}>`;
956
+ const rowOpen = (at, row) =>
957
+ `<row r="${at}"` +
958
+ (row.hidden ? ' hidden="1"' : "") +
959
+ (row.level === 0 ? "" : ` outlineLevel="${row.level}"`) +
960
+ (row.collapsed ? ' collapsed="1"' : "") +
961
+ ">";
758
962
 
759
963
  /**
760
964
  * The sheet's `sheetFormatPr`, or nothing for a sheet that outlines no row.
@@ -976,6 +1180,15 @@ const requireTitles = (rows) => {
976
1180
  );
977
1181
  };
978
1182
 
1183
+ /** @param {string} key @returns {(text: unknown) => void} */
1184
+ const requireFurniture = (key) => (text) => {
1185
+ const stored = furniture(text, `print: ${key}`);
1186
+ if (stored.length > MAX_FURNITURE)
1187
+ throw RangeError(
1188
+ `print: ${key} stores as ${stored.length} characters, and a reader holds at most ${MAX_FURNITURE}`,
1189
+ );
1190
+ };
1191
+
979
1192
  /** @param {unknown} orientation */
980
1193
  const requireOrientation = (orientation) => {
981
1194
  if (!ORIENTATION.has(/** @type {string} */ (orientation)))
@@ -997,6 +1210,10 @@ const PRINT_CHECKS = {
997
1210
  size: requirePaper,
998
1211
  orientation: requireOrientation,
999
1212
  titles: requireTitles,
1213
+ header: requireFurniture("header"),
1214
+ footer: requireFurniture("footer"),
1215
+ firstHeader: requireFurniture("firstHeader"),
1216
+ firstFooter: requireFurniture("firstFooter"),
1000
1217
  };
1001
1218
 
1002
1219
  /**
@@ -1029,7 +1246,7 @@ const worksheet = (name, styles, sst, knows) => {
1029
1246
  let columns = [];
1030
1247
  // What `print` was told, or null. Nothing reaches the file until a caller
1031
1248
  // asks: a reader's own print defaults are better than this writer guessing.
1032
- /** @type {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number } | null} */
1249
+ /** @type {PrintSetup | null} */
1033
1250
  let printing = null;
1034
1251
 
1035
1252
  /**
@@ -1062,13 +1279,13 @@ const worksheet = (name, styles, sst, knows) => {
1062
1279
  const at = rows.length + 1;
1063
1280
  if (cells.length > MAX_COLUMN)
1064
1281
  throw RangeError(`row: a sheet holds at most ${MAX_COLUMN} columns`);
1065
- const level = requireLevel(options);
1066
- deepest = Math.max(deepest, level);
1282
+ const row = requireRow(options);
1283
+ deepest = Math.max(deepest, row.level);
1067
1284
 
1068
1285
  let body = "";
1069
1286
  for (let index = 0; index < cells.length; index++) body += cellAt(cells[index], index, at);
1070
1287
  widest = Math.max(widest, cells.length);
1071
- rows.push(rowOpen(at, level) + body + "</row>");
1288
+ rows.push(rowOpen(at, row) + body + "</row>");
1072
1289
  return at;
1073
1290
  },
1074
1291
 
@@ -1094,7 +1311,7 @@ const worksheet = (name, styles, sst, knows) => {
1094
1311
  * never calls this carries no print setup at all: a reader's own defaults
1095
1312
  * beat a guess.
1096
1313
  *
1097
- * @param {{ margin?: number, size?: string, orientation?: string, fit?: boolean, titles?: number }} setup
1314
+ * @param {PrintSetup} setup
1098
1315
  */
1099
1316
  print(setup) {
1100
1317
  const declared = requireRecord(setup, "print", "a setup object");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "werkmap",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Tiny, CSP-safe OOXML spreadsheet writer. Write-only, zero dependencies, byte-identical output.",
5
5
  "keywords": [
6
6
  "csp",
@@ -57,7 +57,7 @@
57
57
  "size-limit": [
58
58
  {
59
59
  "path": "lib/index.js",
60
- "limit": "7.5 kB"
60
+ "limit": "8 kB"
61
61
  }
62
62
  ],
63
63
  "engines": {