@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/README.md +186 -81
- package/package.json +2 -2
- package/src/check.js +1 -1
- package/src/data.js +21 -23
- package/src/doctor.js +4 -4
- package/src/history.js +3 -9
- package/src/init.js +2 -1
- package/src/locales.js +17 -11
- package/src/metrics.js +45 -63
- package/src/minify.js +2 -2
- package/src/modes.js +8 -8
- package/src/optional.js +2 -2
- package/src/page/app.css +33 -8
- package/src/page/app.js +139 -71
- package/src/page/build.js +166 -19
- package/src/page/panel.js +135 -30
- package/src/page/payload.js +168 -0
- package/src/page/state.js +88 -63
- package/src/page/table.js +280 -73
- package/src/page/work.js +55 -0
- package/src/parse.js +1 -1
- package/src/project.js +2 -2
- package/src/strip/guard.js +4 -3
- package/src/strip.js +4 -5
- package/src/table.css +44 -8
- package/src/tokens.js +5 -6
- package/templates/README.md +3 -3
package/src/page/app.js
CHANGED
|
@@ -1,101 +1,169 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { appUnpack } from './payload.js';
|
|
2
|
+
import { appApply, appBoot, appData, appFoldRead, appLinkUse, appNotice, appRead, appUi, appView, appWrite } from './state.js';
|
|
3
|
+
import { appColumn, appColumnSize, appColumnStale, appContribute, appMetrics, appState, appTable, appTotals, appTotalsReset } from './table.js';
|
|
4
|
+
import { appDraw } from './work.js';
|
|
5
|
+
import { appPanel, appPanelAll, appPanelState } from './panel.js';
|
|
4
6
|
|
|
5
|
-
/* Assembling the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
|
|
7
|
+
/* Assembling the report: the table is built once (`appTable` of the table chapter) and everything afterwards only
|
|
8
|
+
* shows, hides and recounts. A click on any switch therefore costs a class, a number and the fields it reached — the
|
|
9
|
+
* whole table used to be destroyed and built again, which was 81 % of the cost of a click and produced a hundred
|
|
10
|
+
* thousand dead nodes for the collector to walk.
|
|
11
|
+
*
|
|
12
|
+
* Hence two paths and no third: `appPaint` draws the whole view (the first drawing, a record from the browser's
|
|
13
|
+
* memory, a link in the address), while `appSwitch`, `appSwitchGroup` and `appSwitchMetric` are what one click on a
|
|
14
|
+
* box does. The numbers are counted on the click itself — that is arithmetic over the data — while the nodes of the
|
|
15
|
+
* columns are handed to the work chapter (`appDraw`), which draws the short work on the click and the long one in the
|
|
16
|
+
* next task with a stripe over the page (`src/page/work.js` says what was measured: the price of a switch is the
|
|
17
|
+
* browser's own relayout of the table, and it is paid once here rather than once per slice). Neither path makes a
|
|
18
|
+
* node.
|
|
19
|
+
*/
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
appState(metrics.length, files.length);
|
|
23
|
-
if (metrics.length === 0) return;
|
|
21
|
+
// The table's cache of node references: made once, at the first drawing.
|
|
22
|
+
let appCache = null;
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/* One file's column drawn from the view — the state is read when the slice runs rather than kept from the click that
|
|
25
|
+
* queued it, so a click that arrives while the queue is running is drawn by the next slice rather than after it. */
|
|
26
|
+
function appDrawColumn(i) {
|
|
27
|
+
appColumn(appCache, i, appView.files[i] === true);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* The columns of a switch that have to be drawn, as the units the work chapter weighs (`{i, units}` — the file and the
|
|
31
|
+
* nodes of its column). Only the columns whose nodes are out of step with the view are asked for (`appColumnStale`): a
|
|
32
|
+
* report opened with everything switched on has nothing to draw, and a column that is already right would cost its
|
|
33
|
+
* nodes again. The count is the table's (`appColumnSize`), because the table is the only place that knows how many
|
|
34
|
+
* nodes a column has. */
|
|
35
|
+
function appColumns(indexes) {
|
|
36
|
+
const todo = indexes.filter((i) => appColumnStale(appCache, i));
|
|
37
|
+
appDraw(appDrawColumn, todo.map((i) => ({ i: i, units: appColumnSize(appCache, i) })));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* The note under the table: what a row is and how the report was made. It does not depend on the choice, so it is
|
|
41
|
+
* written once — with the table rather than with every drawing of it. */
|
|
42
|
+
function appNote() {
|
|
27
43
|
document.getElementById('note').textContent = appUi.note
|
|
28
44
|
.replace('{rows}', appData.rows.length)
|
|
29
45
|
.replace('{command}', appData.report.fixCommand);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* What the empty states are told: how many metrics and how many files are left. The table stands there in either
|
|
49
|
+
* case (it is built once) — the words are about what is shown. */
|
|
50
|
+
export function appCounts() {
|
|
51
|
+
appState(appData.metrics.filter((m) => appView.metrics[m.key] === true).length,
|
|
52
|
+
appView.files.filter((on) => on === true).length);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The whole view drawn: every column, the totals of the whole selection, the metrics, the empty states and the panel's
|
|
56
|
+
* fields. This is what a link, a record from the memory and the first drawing need — and it makes no node either. */
|
|
57
|
+
export function appPaint() {
|
|
58
|
+
appColumns(appData.files.map((_f, i) => i));
|
|
59
|
+
appTotalsReset(appCache);
|
|
60
|
+
appMetrics(appCache);
|
|
61
|
+
appCounts();
|
|
62
|
+
appPanelAll();
|
|
30
63
|
appWrite();
|
|
31
64
|
}
|
|
32
65
|
|
|
33
|
-
/*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
/* One file switched by the reader: the view, its share of the totals, the fields it shows in and its column — the
|
|
67
|
+
* last through the queue, because a column may be long. The message about a link fades here: by this action the
|
|
68
|
+
* reader has read it. */
|
|
69
|
+
export function appSwitch(i, on) {
|
|
70
|
+
if (appView.files[i] === on) return;
|
|
71
|
+
appView.files[i] = on;
|
|
72
|
+
appContribute(appCache, i, on);
|
|
73
|
+
appTotals(appCache);
|
|
74
|
+
appCounts();
|
|
75
|
+
appPanelState([i]);
|
|
76
|
+
appWrite();
|
|
77
|
+
appNotice('');
|
|
78
|
+
appColumns([i]);
|
|
44
79
|
}
|
|
45
80
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
81
|
+
/* A group switched at once — a folder or a category: the same work per file, then the totals once and the fields of
|
|
82
|
+
* the files the choice really reached (switching a folder on when a part of it was already on touches only the rest,
|
|
83
|
+
* and a field that did not move is not written). The columns of the whole group go into the queue together, so the bar
|
|
84
|
+
* counts them as one piece of work. */
|
|
85
|
+
export function appSwitchGroup(indexes, on) {
|
|
86
|
+
const touched = indexes.filter((i) => appView.files[i] !== on);
|
|
87
|
+
touched.forEach((i) => {
|
|
88
|
+
appView.files[i] = on;
|
|
89
|
+
appContribute(appCache, i, on);
|
|
50
90
|
});
|
|
91
|
+
appTotals(appCache);
|
|
92
|
+
appCounts();
|
|
93
|
+
appPanelState(touched);
|
|
94
|
+
appWrite();
|
|
95
|
+
appNotice('');
|
|
96
|
+
appColumns(touched);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* One metric switched: a class on the table and the headings' `colSpan`. The totals do not move with a metric — they
|
|
100
|
+
* are sums over files — and the metric's own field is the box the reader just clicked. */
|
|
101
|
+
export function appSwitchMetric() {
|
|
102
|
+
appMetrics(appCache);
|
|
103
|
+
appCounts();
|
|
104
|
+
appWrite();
|
|
105
|
+
appNotice('');
|
|
51
106
|
}
|
|
52
107
|
|
|
53
|
-
/* The
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
* does not change between rebuilds. The focus is set without scrolling (`preventScroll`): it returns the keyboard
|
|
57
|
-
* rather than moving the list. */
|
|
58
|
-
function appRender(keepNotice) {
|
|
59
|
-
const at = Array.from(document.querySelectorAll('#panel input')).indexOf(document.activeElement);
|
|
60
|
-
const saved = appScrollTop();
|
|
108
|
+
/* The first drawing: the choice is already in the view (the link and the memory are applied above), the panel is
|
|
109
|
+
* built to match it, the table is built once — every column of every file — and the view is painted over it. */
|
|
110
|
+
function appFirst() {
|
|
61
111
|
appPanel();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/* The message about the link survives the very drawing it caused, and fades on the reader's next action: he has read
|
|
66
|
-
* it by then. */
|
|
67
|
-
if (keepNotice !== true) appNotice('');
|
|
112
|
+
appCache = appTable(document.getElementById('grid'));
|
|
113
|
+
appNote();
|
|
114
|
+
appPaint();
|
|
68
115
|
}
|
|
69
116
|
|
|
117
|
+
/* The page's one asynchronous step, and why there is one. The block in the artifact is packed, and the platform's own
|
|
118
|
+
* unpacker answers with a promise, so the first drawing waits for it; everything after the first drawing is as
|
|
119
|
+
* synchronous as it was, and a click costs what it cost. A host that cannot unpack is told in words instead of being
|
|
120
|
+
* left with an empty table — the reader would not know whether the report or the browser is at fault. */
|
|
121
|
+
let appBooted = false;
|
|
122
|
+
|
|
70
123
|
/* Restoring happens before the first drawing: for someone opening the page for the first time the view has to be the
|
|
71
124
|
* default rather than someone else's choice. A link outranks the memory: it is the sender's explicit choice, and
|
|
72
125
|
* while the reader has changed nothing it does not replace his own — writing it to the memory is what does not
|
|
73
126
|
* happen. A refused link is not an empty table but a message: the reader sees both what happened and what is shown
|
|
74
127
|
* instead. */
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
128
|
+
async function appBegin() {
|
|
129
|
+
try {
|
|
130
|
+
appBoot(await appUnpack(document.getElementById('data')));
|
|
131
|
+
} catch (_e) {
|
|
132
|
+
appNotice(appUi.unpack);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const appStart = appLinkUse();
|
|
136
|
+
/* A link that came in is the sender's choice rather than the reader's: while it is drawn, the memory is not touched
|
|
137
|
+
* (`appWrite`), so opening a link does not make it the reader's own. */
|
|
138
|
+
appTransient = appStart === 'ours';
|
|
139
|
+
if (appStart !== 'ours') {
|
|
140
|
+
const appSaved = appRead();
|
|
141
|
+
if (appSaved !== null) appApply(appSaved);
|
|
142
|
+
}
|
|
143
|
+
/* The unfolded tree is the onlooker's memory rather than the reader's choice: it comes back even when someone
|
|
144
|
+
* else's link is open (otherwise a link sent over would unfold the tree again on every visit). */
|
|
145
|
+
appFoldRead();
|
|
146
|
+
appFirst();
|
|
147
|
+
appBooted = true;
|
|
148
|
+
appTransient = false;
|
|
81
149
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
appStartup = false;
|
|
87
|
-
appForeign = false;
|
|
88
|
-
appTransient = false;
|
|
150
|
+
|
|
151
|
+
/* Whoever opened the page and has to know when the first drawing is over waits for this promise — the checks do
|
|
152
|
+
* (`tools/page-harness.js`); the page itself has no use for it. */
|
|
153
|
+
window.appDrawn = appBegin();
|
|
89
154
|
|
|
90
155
|
/* The anchor changed on an open page: the choice in the new address is applied by the same code as at opening. The
|
|
91
156
|
* page's own address raises no such event (`replaceState` does not), so there is no loop here. A refusal touches
|
|
92
157
|
* neither the view — the reader keeps looking at what he looked at — nor the address: it was sent to the reader,
|
|
93
|
-
* and until he acts it is not ours. */
|
|
158
|
+
* and until he acts it is not ours. The message about the link stays: this drawing is exactly what it explains. */
|
|
94
159
|
window.addEventListener('hashchange', () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
160
|
+
/* The first drawing has not happened yet: the address the page was opened with is the business of that drawing,
|
|
161
|
+
* and a change that arrives before it has drawn nothing to replace. */
|
|
162
|
+
if (!appBooted) return;
|
|
163
|
+
/* An address that came in from outside is read here the way it is read at opening, and a link is not the reader's
|
|
164
|
+
* choice until he changes something — the memory stays his own. The address itself is never rewritten: the page has
|
|
165
|
+
* no business in the tab's title bar, and what a link holds is read rather than made. */
|
|
166
|
+
appTransient = appLinkUse() === 'ours';
|
|
167
|
+
appPaint();
|
|
100
168
|
appTransient = false;
|
|
101
169
|
});
|
package/src/page/build.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import zlib from 'zlib';
|
|
2
3
|
import { fill, LOCALES } from '../locales.js';
|
|
3
4
|
import { PAGE_CSS, TABLE_CSS } from '../css.js';
|
|
5
|
+
import { assertCompilable, stripCss, stripJs, stripLines } from '../strip.js';
|
|
4
6
|
|
|
5
7
|
/* Escaping text for markup lives here, because this builder is the only place that turns data into markup: the rest
|
|
6
8
|
* is drawn as nodes by the page. */
|
|
@@ -17,6 +19,9 @@ export function esc(s) {
|
|
|
17
19
|
* `import` with, and the declarations have to reach the shared scope in the order of pasting — the calculation first,
|
|
18
20
|
* then the chapters.
|
|
19
21
|
*
|
|
22
|
+
* What is pasted is **squeezed** on the way in (`squeezedJs`, `squeezedCss`): comments and indentation leave only the
|
|
23
|
+
* copy inside the report, which is read in a browser, while the sources stay the ordinary files a person edits.
|
|
24
|
+
*
|
|
20
25
|
* The chapters follow the page's subjects, and the order of pasting (the list below) is the order of declarations in
|
|
21
26
|
* the assembled program: the calculation first, then the choice's state, the nodes, the panel, the table and the
|
|
22
27
|
* assembling. The chapters are **slices of one text**: pasting glues them in a row, so the assembled page would stay
|
|
@@ -24,7 +29,10 @@ export function esc(s) {
|
|
|
24
29
|
export function stripModules(src) {
|
|
25
30
|
return src.split('\n')
|
|
26
31
|
.filter((line) => !/^import\s.*;\s*$/.test(line))
|
|
27
|
-
|
|
32
|
+
/* `async` is part of the form rather than a decoration of it: a declaration marked `export async` is as ordinary
|
|
33
|
+
* a source as the rest, and the pasted page has no module syntax to resolve either of them with. */
|
|
34
|
+
.map((line) => line.replace(/^export\s+(?:async\s+)?(?:function|const|let|var|class)\s/,
|
|
35
|
+
(head) => head.slice('export '.length)))
|
|
28
36
|
.join('\n');
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -33,11 +41,32 @@ export function pageSource(file) {
|
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/* The list of chapters lives here rather than in the tests: one copy for the builder and for the guard
|
|
36
|
-
* (`test/page-view.test.js` reads the same program and compares it with the sources).
|
|
37
|
-
|
|
44
|
+
* (`test/page-view.test.js` reads the same program and compares it with the sources). The payload chapter comes
|
|
45
|
+
* first of the page's own: it is what turns the block into the data everything else reads. */
|
|
46
|
+
export const PAGE_PARTS = ['./payload.js', './state.js', './dom.js', './work.js', './panel.js', './table.js', './app.js'];
|
|
47
|
+
|
|
48
|
+
/* The form the artifact carries: the same stripping the `min` metric counts (`src/strip.js`) — comments out,
|
|
49
|
+
* indentation and blank lines out — applied to what is pasted, while the sources on disk keep everything: they are
|
|
50
|
+
* read, edited and linted by people, and only the copy inside the report is squeezed. Exported because the checks
|
|
51
|
+
* compare the page with exactly this form rather than with the sources. */
|
|
52
|
+
export function squeezedJs(code) {
|
|
53
|
+
return stripLines(stripJs(code));
|
|
54
|
+
}
|
|
38
55
|
|
|
56
|
+
export function squeezedCss(css) {
|
|
57
|
+
return stripLines(stripCss(css));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* The page's program: the chapters glued, the module syntax stripped line by line, the text squeezed — in that
|
|
61
|
+
* order, because the stripping works line by line and a comment could otherwise hide a line's shape. The squeeze
|
|
62
|
+
* may throw away nothing but comments and air, so what comes out has to parse, and the same guard the `min` metric
|
|
63
|
+
* uses says so: with no revision (the text is built here and now) and without the unparsed original, because a text
|
|
64
|
+
* that does not parse at all is not "a damaged file" but a bug of this assembler. */
|
|
39
65
|
export function pageScript() {
|
|
40
|
-
|
|
66
|
+
const code = squeezedJs(pageSource('../derived.js') + '\n'
|
|
67
|
+
+ PAGE_PARTS.map((part) => pageSource(part)).join(''));
|
|
68
|
+
assertCompilable(code, '', 'the page’s program');
|
|
69
|
+
return code;
|
|
41
70
|
}
|
|
42
71
|
|
|
43
72
|
/* The note under the heading: what built the report and where it lies. The path is plain text rather than a link: the
|
|
@@ -67,11 +96,9 @@ function uiText(page, loc) {
|
|
|
67
96
|
linkForeign: page.linkForeign,
|
|
68
97
|
linkBroken: page.linkBroken,
|
|
69
98
|
linkExtra: page.linkExtra,
|
|
70
|
-
/*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
approximate: page.approximate,
|
|
74
|
-
approxCell: page.approximateCell,
|
|
99
|
+
/* What a host that cannot unpack the block is told: the page's one message about its own file rather than about
|
|
100
|
+
* the report's numbers (see `appBegin`). */
|
|
101
|
+
unpack: page.unpack,
|
|
75
102
|
/* Why a file is not in the report, in words: the engine names the reason with a mark (`why`), and the page dresses
|
|
76
103
|
* the mark in text, as it does with everything else in the panel. */
|
|
77
104
|
notMeasuredRule: page.notMeasuredRule,
|
|
@@ -91,19 +118,133 @@ function uiText(page, loc) {
|
|
|
91
118
|
};
|
|
92
119
|
}
|
|
93
120
|
|
|
94
|
-
/*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
121
|
+
/* The page's block is the contract in **sparse form** (`schema: 2`): the history as changes rather than as a snapshot
|
|
122
|
+
* per commit, the texts in a dictionary of their own, and the rows' links cut by the part they share. The dense form
|
|
123
|
+
* stays what `--data` answers with — an agent reads the contract as it is — and the page's own chapter unrolls this
|
|
124
|
+
* one (`src/page/payload.js`, `appDecode`), so the two places it is read are the encoder here and the decoder there.
|
|
125
|
+
* `test/contract-data.test.js` holds the round trip between them.
|
|
126
|
+
*
|
|
127
|
+
* What the block leaves out besides the sparse form is the list of skipped commits: it changes with the report's own
|
|
128
|
+
* commit (one with nothing to say lands in the list), and the file would stop being a **fixed point** — a rebuild
|
|
129
|
+
* after its own commit would yield different bytes and the hook would commit the report forever. The page has no use
|
|
130
|
+
* for the list at all: it does not show it. It stays available to the reader — `--data`, `--json` and `explain`
|
|
131
|
+
* answer from the same run.
|
|
132
|
+
*
|
|
133
|
+
* How much this is worth: the artifact of this repository carried 1 370 627 B of data as a snapshot per commit — 95 %
|
|
134
|
+
* of the whole file — while nine tenths of the cells repeat the row above; the same history as changes is about 84 000
|
|
135
|
+
* B. Reproducibility is untouched: the artifact stays a fixed point, rebuilt byte for byte after its own commit. */
|
|
136
|
+
|
|
137
|
+
/* The block's fields, in the order the encoder writes them. The shape is closed: a field added here has to be read in
|
|
138
|
+
* the decoder, and the page's checks compare the block with this list rather than with a description of it. */
|
|
139
|
+
export const PAGE_KEYS = ['schema', 'tool', 'report', 'hrefPrefix', 'strs', 'metrics', 'cats', 'files',
|
|
140
|
+
'catalog', 'rows', 'last', 'hist'];
|
|
141
|
+
|
|
142
|
+
/* The texts of the block, each written once. The dictionary is extended in the order of the walk `pagePayload` makes
|
|
143
|
+
* — files in the column order, rows in the history order — and the order of the first appearance is what decides an
|
|
144
|
+
* index. That is part of the format rather than a detail: the artifact is rebuilt after every commit and has to come
|
|
145
|
+
* out byte-identical on any machine. */
|
|
146
|
+
function dictionary() {
|
|
147
|
+
const list = [];
|
|
148
|
+
const at = new Map();
|
|
149
|
+
function of(text) {
|
|
150
|
+
if (text === null) return null;
|
|
151
|
+
const seen = at.get(text);
|
|
152
|
+
if (seen !== undefined) return seen;
|
|
153
|
+
list.push(text);
|
|
154
|
+
at.set(text, list.length - 1);
|
|
155
|
+
return list.length - 1;
|
|
156
|
+
}
|
|
157
|
+
return { list: list, of: of };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* What the rows' links have in common: for an ordinary row the commit template with the sha cut out, and, when some
|
|
161
|
+
* rows lead to a journal section instead, the part the two kinds share. A row keeps only the rest of its link — which
|
|
162
|
+
* for an ordinary row is the sha the row carries anyway, so a link costs nothing beyond the prefix written once. */
|
|
163
|
+
function linkPrefix(rows) {
|
|
164
|
+
const links = rows.map((r) => r.href).filter((href) => href !== null);
|
|
165
|
+
let out = links.length === 0 ? null : links[0];
|
|
166
|
+
links.forEach((href) => {
|
|
167
|
+
let n = 0;
|
|
168
|
+
while (n < out.length && href[n] === out[n]) n++;
|
|
169
|
+
out = out.slice(0, n);
|
|
170
|
+
});
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* One file between two commits: nothing when the numbers are the same, the absolute numbers when the file was absent
|
|
175
|
+
* before, the deltas when it moved, and a record of the row alone when it is gone — the three shapes the decoder
|
|
176
|
+
* reads. A commit that did not touch a file has no record for it, which is what makes the block small: the numbers are
|
|
177
|
+
* compared rather than the list of paths a commit changed. */
|
|
178
|
+
function change(was, nums) {
|
|
179
|
+
if (nums === null) return was === null ? null : [];
|
|
180
|
+
if (was === null) return nums;
|
|
181
|
+
const deltas = nums.map((n, mi) => n - was[mi]);
|
|
182
|
+
return deltas.every((d) => d === 0) ? null : deltas;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The history of every file, in the column order: the rows it appeared in, moved in and disappeared in.
|
|
186
|
+
function history(keys, files, rows) {
|
|
187
|
+
const was = files.map(() => null);
|
|
188
|
+
const hist = files.map(() => []);
|
|
189
|
+
rows.forEach((row, r) => {
|
|
190
|
+
row.values.forEach((v, i) => {
|
|
191
|
+
const nums = v === null ? null : keys.map((key) => v[key]);
|
|
192
|
+
const rec = change(was[i], nums);
|
|
193
|
+
if (rec !== null) hist[i].push([r].concat(rec));
|
|
194
|
+
was[i] = nums;
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return hist;
|
|
198
|
+
}
|
|
100
199
|
|
|
101
200
|
export function pagePayload(data) {
|
|
102
|
-
const
|
|
103
|
-
|
|
201
|
+
const keys = data.metrics.map((m) => m.key);
|
|
202
|
+
const dict = dictionary();
|
|
203
|
+
const prefix = linkPrefix(data.rows);
|
|
204
|
+
const out = {
|
|
205
|
+
schema: 2,
|
|
206
|
+
tool: data.tool,
|
|
207
|
+
/* The report's own words are the ones the page reads: `heading` is the artifact's `<h1>` and `journal` is null
|
|
208
|
+
* today, so neither is carried — the page builds no heading and prints no journal. */
|
|
209
|
+
report: {
|
|
210
|
+
locale: data.report.locale,
|
|
211
|
+
title: data.report.title,
|
|
212
|
+
artifact: data.report.artifact,
|
|
213
|
+
fixCommand: data.report.fixCommand,
|
|
214
|
+
showSha: data.report.showSha
|
|
215
|
+
},
|
|
216
|
+
hrefPrefix: prefix,
|
|
217
|
+
strs: dict.list,
|
|
218
|
+
metrics: data.metrics.map((m) => [dict.of(m.key), dict.of(m.label), dict.of(m.note), dict.of(m.method)]),
|
|
219
|
+
cats: data.categories.map((c) => [dict.of(c.key), dict.of(c.label)]),
|
|
220
|
+
files: data.files.map((f) => [dict.of(f.label), dict.of(f.path),
|
|
221
|
+
f.paths.map((p) => dict.of(p)), dict.of(f.category), dict.of(f.categoryBy)]),
|
|
222
|
+
catalog: data.catalog.map((e) => [dict.of(e.path), dict.of(e.why)]),
|
|
223
|
+
rows: data.rows.map((r) => [dict.of(r.sha), dict.of(r.when), dict.of(r.subject),
|
|
224
|
+
r.section === null ? null : dict.of(r.section.id),
|
|
225
|
+
r.section === null ? null : dict.of(r.section.head),
|
|
226
|
+
r.section !== null && r.section.added ? 1 : 0,
|
|
227
|
+
r.href === null ? null : dict.of(r.href.slice(prefix.length))]),
|
|
228
|
+
last: [],
|
|
229
|
+
hist: history(keys, data.files, data.rows)
|
|
230
|
+
};
|
|
231
|
+
data.last.forEach((on, i) => { if (on) out.last.push(i); });
|
|
104
232
|
return out;
|
|
105
233
|
}
|
|
106
234
|
|
|
235
|
+
/* The block as the artifact carries it: gzip of its JSON, base64. The packing is a **transport, not the shape** —
|
|
236
|
+
* what the page unpacks is the very block it received before (`schema: 2`), and `--data`/`--json` keep answering
|
|
237
|
+
* with the dense contract. It is here because the block is most of the file: this repository's report carried
|
|
238
|
+
* 88 786 B of it against 32 142 B compressed, 42 856 B with base64's third.
|
|
239
|
+
*
|
|
240
|
+
* Level 9 because the block is written once per commit and read by whoever opens the file, not by a server under
|
|
241
|
+
* load. What the fixed point rests on is that the bytes are reproducible: zlib writes no time into the gzip header,
|
|
242
|
+
* so the same text gives the same bytes (the version of zlib is the machine's, like the version of git). Base64 is
|
|
243
|
+
* what makes the bytes survive a text file — a gzip stream is not valid UTF-8, and the report is one. */
|
|
244
|
+
export function pagePacked(text) {
|
|
245
|
+
return zlib.gzipSync(Buffer.from(text, 'utf8'), { level: 9 }).toString('base64');
|
|
246
|
+
}
|
|
247
|
+
|
|
107
248
|
/* The report's page is one file: the data lies in it, the script is pasted in, there are no external references. Hence
|
|
108
249
|
* it opens with a double click and works without a network. */
|
|
109
250
|
export function pageHtml(data, cfg) {
|
|
@@ -111,15 +252,21 @@ export function pageHtml(data, cfg) {
|
|
|
111
252
|
return '<!doctype html>\n<html lang="' + esc(loc.html) + '">\n<head>\n<meta charset="utf-8">\n'
|
|
112
253
|
+ '<meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
113
254
|
+ '<title>' + esc(data.report.title) + '</title>\n<style>\n'
|
|
114
|
-
+ TABLE_CSS + '\n' + PAGE_CSS + '\n</style>\n</head>\n<body>\n'
|
|
255
|
+
+ squeezedCss(TABLE_CSS) + '\n' + squeezedCss(PAGE_CSS) + '\n</style>\n</head>\n<body>\n'
|
|
115
256
|
+ '<header>\n<h1>' + esc(data.report.heading) + '</h1>\n'
|
|
116
257
|
+ '<p class="sub">' + esc(subText(data, loc.page)) + '</p>\n</header>\n'
|
|
258
|
+
/* The stripe of the drawing stands first in the page and is fixed to the window's top edge rather than laid out
|
|
259
|
+
* with the rest: the page's shape is a grid of five rows (`src/page/app.css`), and a stripe that appeared and
|
|
260
|
+
* disappeared inside it would move the numbers under the reader's eyes every time a switch is drawn. It holds an
|
|
261
|
+
* empty child rather than a share: the length of an indeterminate stripe is the styling's business. */
|
|
262
|
+
+ '<div id="bar" class="bar" role="progressbar" aria-label="' + esc(loc.page.working) + '" hidden><i></i></div>\n'
|
|
117
263
|
+ '<div id="panel" class="panel"></div>\n'
|
|
118
264
|
+ '<p id="notice" class="notice" hidden></p>\n'
|
|
119
265
|
+ '<div id="shell" class="shell"><table id="grid"></table></div>\n'
|
|
120
266
|
+ '<p id="state" class="state" hidden></p>\n'
|
|
121
267
|
+ '<p id="note" class="note"></p>\n'
|
|
122
|
-
+ '<script type="application/
|
|
268
|
+
+ '<script type="application/octet-stream" id="data" data-pack="base64+gzip">'
|
|
269
|
+
+ pagePacked(jsonInHtml(pagePayload(data))) + '</script>\n'
|
|
123
270
|
+ '<script type="application/json" id="ui">' + jsonInHtml(uiText(loc.page, loc)) + '</script>\n'
|
|
124
271
|
+ '<script>\n' + pageScript() + '</script>\n</body>\n</html>\n';
|
|
125
272
|
}
|