@vernikr/size-report 2.5.0 → 2.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/src/page/panel.js CHANGED
@@ -7,12 +7,11 @@ import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './s
7
7
  function appFileBox(i) {
8
8
  const f = appData.files[i];
9
9
  const where = appFileAt(i) + (f.path === null ? appUi.notOnHead : '');
10
- return appBox(f.label, where + appUi.category
10
+ const box = appBox(f.label, where + appUi.category
11
11
  + (f.categoryBy === 'config' ? appUi.categoryFromConfig : appUi.categoryByExtension),
12
- appView.files[i], (e) => {
13
- appView.files[i] = e.target.checked;
14
- appRender();
15
- });
12
+ appView.files[i], (e) => appSwitch(i, e.target.checked));
13
+ appFields.file[i] = box.querySelector('input');
14
+ return box;
16
15
  }
17
16
 
18
17
  /* A file that is not in the report: it stands in its place in the tree with its checkbox off and unavailable — no
@@ -48,7 +47,7 @@ function appNode() {
48
47
  * a fraction ("2/5"): what matters to the reader is that the folder holds five files while two are measured. A folder
49
48
  * without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
50
49
  * in it. */
51
- function appDirHead(name, sub) {
50
+ function appDirHead(name, here, sub) {
52
51
  const idx = appIndexes(sub);
53
52
  const total = appCount(sub);
54
53
  const label = name + '/';
@@ -59,37 +58,49 @@ function appDirHead(name, sub) {
59
58
  const on = idx.map((i) => appView.files[i]);
60
59
  const every = on.every((v) => v);
61
60
  head = appBox(label, appUi.dir.replace('{name}', name).replace('{n}', idx.length),
62
- every, (e) => {
63
- idx.forEach((i) => { appView.files[i] = e.target.checked; });
64
- appRender();
65
- }, 'dir');
61
+ every, (e) => appSwitchGroup(idx, e.target.checked), 'dir');
66
62
  head.querySelector('input').indeterminate = !every && on.some((v) => v);
63
+ appFields.dir[here] = head;
67
64
  }
68
65
  head.appendChild(appEl('span', 'n', idx.length === total ? String(total) : idx.length + '/' + total));
69
66
  return head;
70
67
  }
71
68
 
69
+ /* The order inside one level, and the two rules of it: a hidden name (a leading dot) stands after every visible one —
70
+ * a project's service files are not what a reader looks for first — and otherwise the alphabet decides. "Other things
71
+ * being equal" is the whole of it: what the report holds stands before what it does not, and that partition is made
72
+ * before the names are compared. The rule is a function of two strings, which is what lets it be checked on its own
73
+ * rather than through a tree of a fixture that has no hidden files in it. */
74
+ export function appName(a, b) {
75
+ const hidden = (name) => (name.charAt(0) === '.' ? 1 : 0);
76
+ if (hidden(a) !== hidden(b)) return hidden(a) - hidden(b);
77
+ return a < b ? -1 : (a > b ? 1 : 0);
78
+ }
79
+
72
80
  /* The folder's sign is a click target of its own, separate from the checkbox: the checkbox answers for the numbers (it
73
81
  * switches the subtree's files on), while the sign answers for how much of the tree is visible. One target for two
74
82
  * different decisions would mean a folder can be folded only together with switching its files on. The sign is drawn
75
83
  * as a span rather than a button and stands beside the label rather than inside it: a label is one click target, and a
76
84
  * control nested in it would be reached as that same target.
77
85
  *
86
+ * The tree opens folded — the sign of an untouched folder says so — and the reader's unfolding is what the memory
87
+ * keeps (`appFoldSet`).
88
+ *
78
89
  * A click on the sign rebuilds nothing: the subtree lies in the markup and a class on the row hides it. A rebuild here
79
90
  * would be honest work for nothing — it counts the whole table (every row by every column) and so pays for numbers
80
91
  * folding does not change. That is why only the three things the reader sees change: the class, the sign and the note
81
92
  * in the memory. */
82
93
  function appFoldBox(name, path) {
83
- const folded = appView.folded[path] === true;
84
- const box = appEl('span', 'fold', folded ? '' : '');
85
- box.title = (folded ? appUi.foldOpen : appUi.foldClose).replace('{name}', name);
94
+ const open = appView.open[path] === true;
95
+ const box = appEl('span', 'fold', open ? '' : '');
96
+ box.title = (open ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
86
97
  box.addEventListener('click', () => {
87
- const now = !(appView.folded[path] === true);
98
+ const now = !(appView.open[path] === true);
88
99
  appFoldSet(path, now);
89
100
  const li = box.closest('li');
90
- if (li !== null) li.classList.toggle('folded', now);
91
- box.textContent = now ? '' : '';
92
- box.title = (now ? appUi.foldOpen : appUi.foldClose).replace('{name}', name);
101
+ if (li !== null) li.classList.toggle('folded', !now);
102
+ box.textContent = now ? '' : '';
103
+ box.title = (now ? appUi.foldClose : appUi.foldOpen).replace('{name}', name);
93
104
  });
94
105
  return box;
95
106
  }
@@ -101,17 +112,17 @@ function appLeaves(node) {
101
112
  node.others.forEach((entry) => {
102
113
  items.push({ name: entry.path.split('/').pop(), i: null, entry: entry });
103
114
  });
104
- return items.sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)));
115
+ return items.sort((a, b) => appName(a.name, b.name));
105
116
  }
106
117
 
107
118
  /* A folder row: the folding sign, the checkbox with the number of files and the subtree. A folded folder differs by
108
119
  * its class alone — the markup stays the same. */
109
120
  function appDir(name, sub, prefix) {
110
121
  const here = prefix === '' ? name : prefix + '/' + name;
111
- const folded = appView.folded[here] === true;
122
+ const folded = appView.open[here] !== true;
112
123
  const li = appEl('li', folded ? 'folded' : null);
113
124
  li.appendChild(appFoldBox(name, here));
114
- li.appendChild(appDirHead(name, sub));
125
+ li.appendChild(appDirHead(name, here, sub));
115
126
  li.appendChild(appTreeList(sub, here));
116
127
  return li;
117
128
  }
@@ -129,7 +140,7 @@ function appLeaf(leaf) {
129
140
  * it does not distract from what is in the table, while it can still be found — in the same place where it was. */
130
141
  function appTreeList(node, prefix) {
131
142
  const list = appEl('ul', 'tree');
132
- const dirs = [...node.dirs.keys()].sort()
143
+ const dirs = [...node.dirs.keys()].sort(appName)
133
144
  .map((name) => ({ name: name, sub: node.dirs.get(name), inReport: appIndexes(node.dirs.get(name)).length > 0 }));
134
145
  const leaves = appLeaves(node);
135
146
  const inside = leaves.filter((leaf) => leaf.entry === null);
@@ -165,6 +176,10 @@ function appTree() {
165
176
  return appTreeList(root, '');
166
177
  }
167
178
 
179
+ /* The panel: built once, at the first drawing, and afterwards only its fields change. A rebuild would count the whole
180
+ * table for nothing — the tree, the counters and the tooltips are the same after every click — while it would also
181
+ * take the reader's place in the list away: the scroll of the panel and of the tree, and the field under the
182
+ * keyboard, would have to be put back by hand. Nothing of that is here, because there is nothing to put back. */
168
183
  export function appPanel() {
169
184
  const panel = document.getElementById('panel');
170
185
  panel.textContent = '';
@@ -173,11 +188,12 @@ export function appPanel() {
173
188
  metrics.appendChild(appEl('legend', null, appUi.metrics));
174
189
  const mrow = appEl('div', 'row');
175
190
  appData.metrics.forEach((m) => {
176
- const word = m.accuracy === 'exact' ? appUi.exact : appUi.approximate;
177
- mrow.appendChild(appBox(m.label, m.note + ' · ' + word, appView.metrics[m.key], (e) => {
191
+ const box = appBox(m.label, m.note, appView.metrics[m.key], (e) => {
178
192
  appView.metrics[m.key] = e.target.checked;
179
- appRender();
180
- }, 'metric'));
193
+ appSwitchMetric();
194
+ }, 'metric');
195
+ appFields.metric[m.key] = box.querySelector('input');
196
+ mrow.appendChild(box);
181
197
  });
182
198
  metrics.appendChild(mrow);
183
199
  /* What produced each number is visible rather than hidden in a tooltip: the token dictionary and the way of
@@ -196,13 +212,102 @@ export function appPanel() {
196
212
  appData.categories.forEach((cat) => {
197
213
  const idx = [];
198
214
  appData.files.forEach((f, i) => { if (f.category === cat.key) idx.push(i); });
199
- cats.appendChild(appBox(cat.label, appUi.all + ' · ' + cat.label, idx.every((i) => appView.files[i]),
200
- (e) => {
201
- idx.forEach((i) => { appView.files[i] = e.target.checked; });
202
- appRender();
203
- }, 'all'));
215
+ const box = appBox(cat.label, appUi.all + ' · ' + cat.label, idx.every((i) => appView.files[i]),
216
+ (e) => appSwitchGroup(idx, e.target.checked), 'all');
217
+ appFields.cat[cat.key] = box.querySelector('input');
218
+ cats.appendChild(box);
204
219
  });
205
220
  files.appendChild(cats);
206
221
  files.appendChild(appTree());
207
222
  panel.appendChild(files);
208
223
  }
224
+
225
+ /* -------- the panel's fields after a choice -------- */
226
+
227
+ /* The fields of the panel by name: the markup is built once, so a click needs a reference to the field it changes
228
+ * rather than a rebuild of the panel. Only a file owns a state — a folder and a category are ways to set the same
229
+ * boxes — which is why their fields are read from the files below them rather than kept. */
230
+ const appFields = { metric: {}, file: {}, dir: {}, cat: {} };
231
+
232
+ /* The boxes of a row's own subtree, read from the tree itself: the panel keeps no second list of the files a folder
233
+ * holds, and what the reader sees is exactly the boxes that are here. A file outside the report stands in the tree
234
+ * with nothing to switch, hence it is left out. */
235
+ function appRowBoxes(box) {
236
+ return [...box.closest('li').querySelectorAll('.box:not(.dir):not(.plain) input')];
237
+ }
238
+
239
+ // A folder's field from its files: all on — checked, some — the third state, none — simply unchecked.
240
+ function appDirState(box) {
241
+ const boxes = appRowBoxes(box);
242
+ const on = boxes.filter((b) => b.checked).length;
243
+ const input = box.querySelector('input');
244
+ input.checked = on === boxes.length;
245
+ input.indeterminate = on > 0 && on < boxes.length;
246
+ }
247
+
248
+ /* A category's field from its files — the same rule, taken from the data: a category's files are named by the
249
+ * category itself (`category`), and the boxes of the tree are a different view of the same files. */
250
+ function appCatState(key) {
251
+ let all = 0;
252
+ let on = 0;
253
+ appData.files.forEach((f, i) => {
254
+ if (f.category !== key) return;
255
+ all++;
256
+ if (appView.files[i] === true) on++;
257
+ });
258
+ const input = appFields.cat[key];
259
+ input.checked = on === all;
260
+ input.indeterminate = on > 0 && on < all;
261
+ }
262
+
263
+ /* The folders a file lies in: the prefixes of its path, from the root down. The tree's folders are exactly those
264
+ * prefixes — that is how it is built (`appLeafAt`) — so no second naming rule is needed. */
265
+ function appDirPath(path) {
266
+ const parts = path.split('/');
267
+ return parts.slice(0, -1).map((_part, i) => parts.slice(0, i + 1).join('/'));
268
+ }
269
+
270
+ /* A click reaches a folder's field through the files below it: every folder on the path of a switched file shows the
271
+ * share of what is left on. A folder without measured files has a field of its own too — it is off and unavailable,
272
+ * which is not the reader's state and must not be overwritten here. */
273
+ function appDirsOf(indexes) {
274
+ const seen = {};
275
+ indexes.forEach((i) => {
276
+ appDirPath(appFileAt(i)).forEach((path) => {
277
+ if (seen[path] === true) return;
278
+ seen[path] = true;
279
+ if (appFields.dir[path] !== undefined) appDirState(appFields.dir[path]);
280
+ });
281
+ });
282
+ }
283
+
284
+ // The same for the quick buttons of the categories: every category one of the switched files belongs to.
285
+ function appCatsOf(indexes) {
286
+ const seen = {};
287
+ indexes.forEach((i) => {
288
+ const key = appData.files[i].category;
289
+ if (seen[key] === true || appFields.cat[key] === undefined) return;
290
+ seen[key] = true;
291
+ appCatState(key);
292
+ });
293
+ }
294
+
295
+ /* What a click changed, written where it stands: the files' own boxes, then the fields of the folders and categories
296
+ * that hold them. Nothing is rebuilt, and no field the choice did not reach is touched. */
297
+ export function appPanelState(indexes) {
298
+ indexes.forEach((i) => {
299
+ const input = appFields.file[i];
300
+ if (input !== undefined) input.checked = appView.files[i] === true;
301
+ });
302
+ appDirsOf(indexes);
303
+ appCatsOf(indexes);
304
+ }
305
+
306
+ /* The whole panel from the view: what a link, a record from the memory and the first drawing need. The metric boxes
307
+ * are the reader's own click otherwise, which is why they are not refreshed on a file's switch. */
308
+ export function appPanelAll() {
309
+ appData.categories.forEach((c) => appCatState(c.key));
310
+ appData.metrics.forEach((m) => { appFields.metric[m.key].checked = appView.metrics[m.key] === true; });
311
+ appData.files.forEach((_f, i) => { appFields.file[i].checked = appView.files[i] === true; });
312
+ Object.keys(appFields.dir).forEach((path) => appDirState(appFields.dir[path]));
313
+ }
@@ -0,0 +1,168 @@
1
+ /* The page's block: how the file carries it (gzipped and base64 encoded, `appUnpack`) and how the block in
2
+ * sparse form is turned back into the model the page already spoke.
3
+ *
4
+ * The packing is a **transport rather than the shape**: what comes out of the unpacker is the very block the
5
+ * page received before this chapter knew about gzip, with no field added or removed, and `--data`/`--json`
6
+ * answer with the dense contract as they always did. It buys the artifact's weight (the block is the whole file)
7
+ * and pays for it in two ways, both on purpose: the block can no longer be read by eye or by `diff`, and
8
+ * unpacking is **asynchronous** — the platform's own `DecompressionStream` is the only unpacker here, no library
9
+ * travels in the page and nothing is fetched, so the first drawing waits for a promise where it used to happen
10
+ * during the parse.
11
+ *
12
+ * Why the block is sparse. Of 56 019 cells of this repository's report the non-empty ones hold 1 183 distinct
13
+ * triples, and about a thousand cells differ from the row above: nine tenths of the block is yesterday's
14
+ * numbers written again. So the block keeps, for every file, the rows in which it appeared (absolute numbers),
15
+ * moved (deltas against its own previous record) or disappeared, and this chapter puts the snapshots back
16
+ * together. The whole history walk is O(number of changes) rather than O(rows × files).
17
+ *
18
+ * The model after `appDecode` is exactly `--data`: the calculation (`rowModel`, `totalsOf`, `cellParts`,
19
+ * `valueParts`), the table and the panel know nothing about the sparse form, so there is no second way to
20
+ * count a row. A value that did not move is **one object shared by the rows that hold it** — the heap keeps
21
+ * the distinct numbers rather than a copy per commit — and because every consumer reads `v[metric]` and
22
+ * compares nothing by identity, sharing changes no answer.
23
+ *
24
+ * Two rules of the chapters bind here as well: no `import` lines and no module state (the text is pasted into
25
+ * one file), and nothing but declarations (the round-trip check evaluates this file on its own).
26
+ *
27
+ * The dictionary (`strs`) is walked by the encoder in a stated order — files in the column order, rows in the
28
+ * history order, and only the order of the first appearance decides an index. The artifact is rebuilt after
29
+ * every commit and has to come out byte-identical on any machine, so the order of the walk is part of the
30
+ * format rather than a detail of the encoder.
31
+ */
32
+
33
+ /* The block as the file carries it: the tag's `data-pack` says which packing it is, so the page asks rather than
34
+ * guesses, and a packing it does not know is an error rather than a half-read block. A tag without the marker
35
+ * carries the block itself — the same page works for a build that packs nothing.
36
+ *
37
+ * `DecompressionStream` answers with streams rather than with bytes, hence the reader loop: the promise this
38
+ * function is *is* the price of the weight (see the file's note). `atob` gives one character per byte, and every
39
+ * character above 127 has to be taken back as a byte (`charCodeAt`) rather than as text; `TextDecoder` turns the
40
+ * inflated bytes into the JSON text, which is UTF-8 with the report's own words in it. */
41
+ export async function appUnpack(el) {
42
+ const pack = el.getAttribute('data-pack');
43
+ const text = el.textContent;
44
+ if (pack === null) return text;
45
+ if (pack !== 'base64+gzip') throw new Error('the page’s data is packed as “' + pack + '”, which this page cannot read');
46
+ const stream = new DecompressionStream('gzip');
47
+ const sink = stream.writable.getWriter();
48
+ /* The writing is not waited for before the reading: a stream that is filled before it is drained would stall on
49
+ * its own backpressure. A failure of the write surfaces in the reading loop, which is what this promise returns. */
50
+ const feeding = sink.write(appBytes(text)).then(() => sink.close()).catch(() => null);
51
+ const source = stream.readable.getReader();
52
+ const parts = [];
53
+ for (;;) {
54
+ const step = await source.read();
55
+ if (step.done) break;
56
+ parts.push(step.value);
57
+ }
58
+ await feeding;
59
+ return new TextDecoder().decode(appJoined(parts));
60
+ }
61
+
62
+ // base64 read as bytes: the browser's `atob` hands out one character per byte, whatever the byte.
63
+ function appBytes(text) {
64
+ const raw = atob(text);
65
+ const bytes = new Uint8Array(raw.length);
66
+ for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
67
+ return bytes;
68
+ }
69
+
70
+ /* The chunks of an inflated stream as one array: a chunk boundary falls wherever the platform put it. The length is
71
+ * summed by hand rather than by `reduce` — the page's shell computes no totals of its own, and this chapter is part
72
+ * of the shell the checks read (`test/page-view.test.js`). */
73
+ function appJoined(parts) {
74
+ let size = 0;
75
+ parts.forEach((part) => { size += part.length; });
76
+ const all = new Uint8Array(size);
77
+ let at = 0;
78
+ parts.forEach((part) => { all.set(part, at); at += part.length; });
79
+ return all;
80
+ }
81
+
82
+ // The dictionary's entry: a text the block does not carry stays absent rather than becoming an empty string.
83
+ function appText(p, i) {
84
+ return i === null ? null : p.strs[i];
85
+ }
86
+
87
+ // A value as the page reads it: the metrics by their keys, in the order the block lists them.
88
+ function appValue(keys, nums) {
89
+ const out = {};
90
+ keys.forEach((key, mi) => { out[key] = nums[mi]; });
91
+ return out;
92
+ }
93
+
94
+ /* One record of a file's history applied to what the file was: the numbers are absolute when the file was
95
+ * absent and deltas against its own previous record otherwise. A record with nothing but a row means the file
96
+ * is gone from that revision, which is the same `null` the dense contract carries. What comes out is the
97
+ * object every later row shares until the file moves again. */
98
+ function appStep(keys, was, rec) {
99
+ if (rec.length === 1) return null;
100
+ const nums = rec.slice(1);
101
+ if (was === null) return appValue(keys, nums);
102
+ return appValue(keys, nums.map((d, mi) => was[keys[mi]] + d));
103
+ }
104
+
105
+ /* The history unrolled, once, into a snapshot per commit — the shape the contract hands out. The records of
106
+ * every file are consumed in the order of the rows, and each row takes what the files were at it: a file whose
107
+ * record has not come around is the very object it was in the row above.
108
+ *
109
+ * The tail of the walk is "now": the report's last row is its last commit that moved a number, the commits
110
+ * after it moved none, and what the files were at the end is the state at HEAD — which is what the dense
111
+ * `now` is. */
112
+ function appUnroll(p, keys) {
113
+ const at = p.files.map(() => 0);
114
+ const live = p.files.map(() => null);
115
+ const rows = [];
116
+ for (let r = 0; r < p.rows.length; r++) {
117
+ const row = [];
118
+ p.files.forEach((_f, i) => {
119
+ const rec = p.hist[i][at[i]];
120
+ if (rec !== undefined && rec[0] === r) {
121
+ live[i] = appStep(keys, live[i], rec);
122
+ at[i]++;
123
+ }
124
+ row.push(live[i]);
125
+ });
126
+ rows.push(row);
127
+ }
128
+ return { rows: rows, now: live.slice() };
129
+ }
130
+
131
+ /* A commit row: the caption and where it leads. The address is the block's one prefix plus what the row kept
132
+ * of its own link with the sha, and `added` travels as a mark rather than as a word. The name is not `appRow`:
133
+ * the chapters are pasted into **one scope**, where the table's own `appRow` would quietly win and this one
134
+ * would never be called (`test/page-view.test.js` holds the names apart for that very reason). */
135
+ function appRowOf(p, r, values) {
136
+ const section = r[3] === null ? null : { id: appText(p, r[3]), head: appText(p, r[4]), added: r[5] === 1 };
137
+ return {
138
+ sha: appText(p, r[0]),
139
+ when: appText(p, r[1]),
140
+ subject: appText(p, r[2]),
141
+ section: section,
142
+ href: r[6] === null ? null : p.hrefPrefix + appText(p, r[6]),
143
+ values: values
144
+ };
145
+ }
146
+
147
+ /* The whole block, unrolled into the dense contract the page reads: `schema: 2` is refused by `appRecordOk`
148
+ * rather than unrolled — a record written for a block of another form describes another choice. */
149
+ export function appDecode(p) {
150
+ const keys = p.metrics.map((m) => appText(p, m[0]));
151
+ const hist = appUnroll(p, keys);
152
+ const last = p.files.map(() => false);
153
+ p.last.forEach((i) => { last[i] = true; });
154
+ return {
155
+ schema: p.schema,
156
+ tool: p.tool,
157
+ report: p.report,
158
+ metrics: p.metrics.map((m) => ({ key: appText(p, m[0]), label: appText(p, m[1]),
159
+ note: appText(p, m[2]), method: appText(p, m[3]) })),
160
+ categories: p.cats.map((c) => ({ key: appText(p, c[0]), label: appText(p, c[1]) })),
161
+ files: p.files.map((f) => ({ label: appText(p, f[0]), path: appText(p, f[1]),
162
+ paths: f[2].map((i) => p.strs[i]), category: appText(p, f[3]), categoryBy: appText(p, f[4]) })),
163
+ catalog: p.catalog.map((e) => ({ path: appText(p, e[0]), why: appText(p, e[1]) })),
164
+ rows: p.rows.map((r, ri) => appRowOf(p, r, hist.rows[ri])),
165
+ now: hist.now,
166
+ last: last
167
+ };
168
+ }
package/src/page/state.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { appDecode } from './payload.js';
2
+
1
3
  /* The page's choice state: what is switched on, how it survives a closing and how it travels as a link.
2
4
  *
3
5
  * An ordinary source file rather than a string inside the engine: a linter sees it, and it is pasted into the assembled
@@ -9,41 +11,61 @@
9
11
  * network: the styling arrives in the same file, and the cell markup follows the rules of the shared part of the styling
10
12
  * (`clip`, a commit's caption).
11
13
  *
12
- * The panel remembers the reader's choice between visits and can hand it over as a link ("the choice's memory" below):
13
- * the record is tied to the report's passport and keeps only what is switched off, by name, so someone else's record is
14
- * not applied while a vanished name simply means nothing. The same record goes into the address which is what one
15
- * sends to a colleague.
14
+ * The panel remembers the reader's choice between visits ("the choice's memory" below): the record is tied to the
15
+ * report's passport and keeps only what is switched off, by name, so someone else's record is not applied while a
16
+ * vanished name simply means nothing. The record stays in the browser's memory and nowhere else: a report opened from
17
+ * disk keeps a clean address, and a link made in an earlier release is still read (`appLinkUse`).
16
18
  *
17
- * The page does not derive a number's accuracy itself: the marks of approximate cells arrive in the data, from the same
18
- * rule that names a metric's accuracy. The page draws no such conclusion from paths and formats there will be no
19
- * second rule of accuracy. */
19
+ * The page draws no conclusion about how a number was obtained: the method of each metric arrives in the data, and the
20
+ * page prints it. There is no second rule of counting here, and no vocabulary of precision either. */
20
21
 
21
- export const appData = JSON.parse(document.getElementById('data').textContent);
22
22
  export const appUi = JSON.parse(document.getElementById('ui').textContent);
23
- export const appView = { metrics: {}, files: [], folded: {} };
24
- appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
25
- appData.files.forEach(() => { appView.files.push(true); });
26
23
 
27
- /* A metric's description by key: the tooltip of an approximate cell names the way its number was obtained the same one
28
- * that stands in the metric's caption, so the page holds no two answers about "counted with what". */
29
- export const appMetric = {};
30
- appData.metrics.forEach((m) => { appMetric[m.key] = m; });
24
+ /* The model is not a constant any longer: the block in the file is **packed** (gzipped and base64 encoded,
25
+ * `appUnpack` of the payload chapter), so unpacking is asynchronous and the model is set once, by `appBoot`,
26
+ * before anything is drawn and before any chapter below reads it. The names, their shapes and their order are what
27
+ * they were; only their appearance moved — from the parse to that one call. */
28
+ export let appData = null;
29
+ export let appView = null;
30
+ export let appMetric = null;
31
+ export let appMeasured = null;
32
+ let appKey = null;
33
+ let appFoldKey = null;
31
34
 
32
- /* A column by a file's path: the page's tree is the project's tree (every path of the catalogue) while the numbers
33
- * belong to columns only, so this pointer is what decides whether a leaf is a checkbox or a caption. The name follows the
34
- * same rule as the choice's record (`appFileAt`), so the tree and the reader's memory cannot drift apart. */
35
- export const appMeasured = {};
36
- appData.files.forEach((_f, i) => { appMeasured[appFileAt(i)] = i; });
35
+ /* One place that turns the unpacked block into what the chapters speak: the sparse form is unrolled by the payload
36
+ * chapter into the dense contract every number comes from snapshots per commit, the texts themselves, "now" as
37
+ * the state at HEAD and the view starts switched on whole.
38
+ *
39
+ * A metric's description by key stands here too: the way the number was obtained is text under the switches, so the
40
+ * page holds no second answer about "counted with what".
41
+ *
42
+ * A column by a file's path is the pointer that decides whether a leaf of the tree is a checkbox or a caption (the
43
+ * page's tree is the project's tree while the numbers belong to columns only); the name follows the same rule as the
44
+ * choice's record (`appFileAt`), so the tree and the reader's memory cannot drift apart.
45
+ *
46
+ * The key the reader's memory lives under is counted here as well, because it is the report's passport: it depends
47
+ * on the data, and until the block is unpacked there is nothing to count it from. */
48
+ export function appBoot(text) {
49
+ appData = appDecode(JSON.parse(text));
50
+ /* The tree is folded as it opens: a project's tree is longer than the window, and the reader's first look is at a
51
+ * short list rather than at everything. What is remembered is the opposite — the folders the reader unfolded
52
+ * (`appFoldRead`). */
53
+ appView = { metrics: {}, files: [], open: {} };
54
+ appMetric = {};
55
+ appMeasured = {};
56
+ appData.metrics.forEach((m) => { appView.metrics[m.key] = true; appMetric[m.key] = m; });
57
+ appData.files.forEach((_f, i) => { appView.files.push(true); appMeasured[appFileAt(i)] = i; });
58
+ appKey = 'size-report:' + appPassport();
59
+ appFoldKey = appKey + ':tree';
60
+ }
37
61
 
38
62
  /* A link is that same choice in the address, under a name of its own: someone else's anchor on the page does not count
39
- * as a link, and there is nothing to argue with it about. */
63
+ * as a link, and there is nothing to argue with it about. The page does not write it any more — it only reads what came
64
+ * with the address — and this paragraph is what such a link is read by. */
40
65
  const APP_LINK = '#size-report=';
41
66
 
42
- /* Three circumstances of the first drawing, which act on it alone: the address is not rewritten during it (it was sent to
43
- * the reader rather than the other way), the memory is not touched (a link that came in is not the reader's choice), and
44
- * the message about the link has not faded yet. */
45
- let appStartup = true;
46
- let appForeign = false;
67
+ /* One circumstance of the first drawing, and it acts on it alone: the memory is not written while somebody else's link
68
+ * is open what came in is not the reader's choice, and only his own action makes it his. */
47
69
  let appTransient = false;
48
70
 
49
71
  /* -------- the reader's memory of his choice -------- */
@@ -70,12 +92,19 @@ function appHash(text) {
70
92
  * report's order. It is what tells one report from another — the record's key is chosen by it, so a choice made in
71
93
  * someone else's report is not picked up. The package version and the top of the history are absent on purpose: this is
72
94
  * the same report — updating the tool does not change what a column means, while a grown history is the very history the
73
- * reader comes back to. */
95
+ * reader comes back to.
96
+ *
97
+ * Counted once per document: it is a constant of the report, which depends on nothing the reader can change, and every
98
+ * click asks for it (the key of the memory and the passport of the record). A second count would be a second answer
99
+ * waiting to happen, and the labels it reads do not change while the page is open. */
100
+ let appPassportValue = null;
74
101
  function appPassport() {
75
- return appHash([appData.tool.name, appData.schema, appData.report.artifact,
76
- appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
102
+ if (appPassportValue === null) {
103
+ appPassportValue = appHash([appData.tool.name, appData.schema, appData.report.artifact,
104
+ appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
105
+ }
106
+ return appPassportValue;
77
107
  }
78
- const appKey = 'size-report:' + appPassport();
79
108
 
80
109
  /* One record of the choice for everything: it goes both into the memory and into the address, so there are no two formats
81
110
  * of one state. Only what is switched off is kept, by name: "switched on" and "no record" are the same state, which is
@@ -93,32 +122,27 @@ function appRecordOk(rec) {
93
122
  return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
94
123
  }
95
124
 
125
+ /* The record goes into the browser's memory and nowhere else: it is written on the click itself, which is what survives
126
+ * a closing, and the page's address keeps a clean tail — the report is a local page whose address is copied as it is,
127
+ * and a reader's choice belongs in the browser that made it rather than in the tab's title bar. What a link sent from an
128
+ * earlier release holds is still read (`appLinkUse`), and it is not written into the reader's memory: what came in is
129
+ * not his choice until he changes something. */
96
130
  export function appWrite() {
131
+ if (appTransient) return;
97
132
  const rec = appRecord();
98
133
  const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
99
- if (!appTransient) {
100
- try {
101
- if (empty) window.localStorage.removeItem(appKey);
102
- else window.localStorage.setItem(appKey, JSON.stringify(rec));
103
- } catch (_e) {
104
- /* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
105
- * and the markup do not depend on it. */
106
- }
107
- }
108
- /* The address is the link for a colleague, which is why it repeats the choice. But not during the first drawing and not
109
- * when the link turned out to be someone else's: an address that came in is not ours, and the reader has yet to read
110
- * it. */
111
- if (appStartup || appForeign) return;
112
134
  try {
113
- window.history.replaceState(null, '', APP_LINK + encodeURIComponent(JSON.stringify(rec)));
135
+ if (empty) window.localStorage.removeItem(appKey);
136
+ else window.localStorage.setItem(appKey, JSON.stringify(rec));
114
137
  } catch (_e) {
115
- /* The browser grants no change of the address: the link is then taken from the browser's memory. */
138
+ /* There is no memory (the browser grants this page none): the choice will not survive a closing, while the numbers
139
+ * and the markup do not depend on it. */
116
140
  }
117
141
  }
118
142
 
119
143
  /* A reset to "everything on": the border between "this is no longer in the report" and "switched off" is the record
120
- * rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view rather
121
- * than on top of someone else's. */
144
+ * rather than a missing value. A link carries the sender's whole choice, which is why it is applied to a clean view
145
+ * rather than on top of someone else's. */
122
146
  function appAll() {
123
147
  appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
124
148
  appData.files.forEach((_f, i) => { appView.files[i] = true; });
@@ -218,14 +242,15 @@ export function appApply(rec) {
218
242
  appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
219
243
  }
220
244
 
221
- /* -------- the folded tree -------- */
222
-
223
- /* Folded folders are a memory of the same kind as the choice, but of a record of their own: it is about how much of the
224
- * tree is visible rather than about which numbers are read. Hence it does not go into the address: a link is sent for the
225
- * sake of the numbers, while an unfolded tree is the onlooker's business. As with the choice, only what is folded is kept
226
- * (`true`), and a folder's name is its path ("src/page"), so a vanished name simply means nothing. */
227
- const appFoldKey = appKey + ':tree';
245
+ /* -------- the unfolded tree -------- */
228
246
 
247
+ /* How much of the tree is visible is a memory of the same kind as the choice, but of a record of its own: it is about
248
+ * what the onlooker looks at rather than about which numbers are read, which is why it never goes into a link and never
249
+ * leaves the browser. The tree opens folded, so **the unfolded folders are what is kept** (`true`) — the default is the
250
+ * absence of the name, the same way "switched on" is the absence of a choice. A folder's name is its path ("src/page"),
251
+ * so a vanished name simply means nothing, and unfolding nothing is the state the page opens in: then the record is not
252
+ * kept at all rather than being kept empty. `appFoldKey` is set with the model (`appBoot`), for the reason the key
253
+ * itself is. */
229
254
  export function appFoldRead() {
230
255
  let text = null;
231
256
  try {
@@ -241,19 +266,19 @@ export function appFoldRead() {
241
266
  return;
242
267
  }
243
268
  if (!appRecordOk(rec)) return;
244
- const folded = rec.folded || {};
245
- Object.keys(folded).forEach((p) => { if (folded[p] === true) appView.folded[p] = true; });
269
+ const open = rec.open || {};
270
+ Object.keys(open).forEach((p) => { if (open[p] === true) appView.open[p] = true; });
246
271
  }
247
272
 
248
- export function appFoldSet(path, folded) {
249
- if (folded) appView.folded[path] = true;
250
- else delete appView.folded[path];
251
- const rec = { v: 1, passport: appPassport(), folded: Object.assign({}, appView.folded) };
273
+ export function appFoldSet(path, open) {
274
+ if (open) appView.open[path] = true;
275
+ else delete appView.open[path];
276
+ const rec = { v: 1, passport: appPassport(), open: Object.assign({}, appView.open) };
252
277
  try {
253
- if (Object.keys(rec.folded).length === 0) window.localStorage.removeItem(appFoldKey);
278
+ if (Object.keys(rec.open).length === 0) window.localStorage.removeItem(appFoldKey);
254
279
  else window.localStorage.setItem(appFoldKey, JSON.stringify(rec));
255
280
  } catch (_e) {
256
- /* There is no memory: what is folded will not survive a closing, while the view does not depend on it — the tree is
257
- * folded exactly the way the reader folded it just now. */
281
+ /* There is no memory: what is unfolded will not survive a closing, while the view does not depend on it — the tree
282
+ * is unfolded exactly the way the reader unfolded it just now. */
258
283
  }
259
284
  }