werkmap 0.6.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
@@ -9,7 +9,7 @@ Writing is the whole surface, so the package stays small enough to audit. The co
9
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
 
package/lib/index.d.ts CHANGED
@@ -152,6 +152,17 @@ export interface RowOptions {
152
152
  * the one below the group. `0`, the default, is a row outside any group.
153
153
  */
154
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;
155
166
  }
156
167
 
157
168
  export interface Sheet {
@@ -160,7 +171,9 @@ export interface Sheet {
160
171
  /**
161
172
  * Append a row. `null` is an empty unstyled cell. Returns the row's 1-based
162
173
  * position, which `merge` and `place` take. `options.level` puts the row at
163
- * 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.
164
177
  */
165
178
  row(cells: readonly (Cell | null)[], options?: RowOptions): number;
166
179
  /**
package/lib/index.js CHANGED
@@ -903,12 +903,29 @@ const MAX_LEVEL = 7;
903
903
  /** @param {unknown} level */
904
904
  const isLevel = (level) => level === 0 || isIndex(level, MAX_LEVEL);
905
905
 
906
- /**
907
- * A row's outline level, or 0 for a row that said nothing.
908
- * @param {unknown} options
909
- */
910
- const requireLevel = (options) => {
911
- 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) => {
912
929
  if (!isLevel(level))
913
930
  throw RangeError(
914
931
  `row: level: expected an integer between 0 and ${MAX_LEVEL}, got ${JSON.stringify(level)}`,
@@ -917,11 +934,31 @@ const requireLevel = (options) => {
917
934
  };
918
935
 
919
936
  /**
920
- * 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.
921
953
  * @param {number} at
922
- * @param {number} level
954
+ * @param {ReturnType<typeof requireRow>} row
923
955
  */
924
- 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
+ ">";
925
962
 
926
963
  /**
927
964
  * The sheet's `sheetFormatPr`, or nothing for a sheet that outlines no row.
@@ -1242,13 +1279,13 @@ const worksheet = (name, styles, sst, knows) => {
1242
1279
  const at = rows.length + 1;
1243
1280
  if (cells.length > MAX_COLUMN)
1244
1281
  throw RangeError(`row: a sheet holds at most ${MAX_COLUMN} columns`);
1245
- const level = requireLevel(options);
1246
- deepest = Math.max(deepest, level);
1282
+ const row = requireRow(options);
1283
+ deepest = Math.max(deepest, row.level);
1247
1284
 
1248
1285
  let body = "";
1249
1286
  for (let index = 0; index < cells.length; index++) body += cellAt(cells[index], index, at);
1250
1287
  widest = Math.max(widest, cells.length);
1251
- rows.push(rowOpen(at, level) + body + "</row>");
1288
+ rows.push(rowOpen(at, row) + body + "</row>");
1252
1289
  return at;
1253
1290
  },
1254
1291
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "werkmap",
3
- "version": "0.6.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",