@vernikr/size-report 2.4.0 → 2.5.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 +929 -1081
- package/bin/postinstall.js +17 -18
- package/bin/size.js +2 -2
- package/package.json +3 -4
- package/src/args.js +72 -72
- package/src/artifact.js +14 -14
- package/src/check.js +41 -42
- package/src/cli.js +26 -29
- package/src/config.js +87 -91
- package/src/css.js +14 -14
- package/src/data.js +31 -37
- package/src/derived.js +31 -35
- package/src/doctor.js +95 -99
- package/src/explain.js +46 -47
- package/src/git.js +66 -71
- package/src/history.js +74 -77
- package/src/hook.js +130 -149
- package/src/init.js +36 -37
- package/src/journal.js +17 -15
- package/src/locales.js +25 -16
- package/src/metrics.js +56 -55
- package/src/minify.js +28 -27
- package/src/modes.js +57 -60
- package/src/optional.js +13 -11
- package/src/page/app.css +75 -91
- package/src/page/app.js +30 -35
- package/src/page/build.js +40 -39
- package/src/page/dom.js +8 -9
- package/src/page/panel.js +48 -51
- package/src/page/state.js +72 -87
- package/src/page/table.js +21 -24
- package/src/parse-worker.js +10 -10
- package/src/parse.js +43 -45
- package/src/project.js +100 -104
- package/src/refusal.js +75 -76
- package/src/size-table.js +41 -76
- package/src/strip/forms.js +5 -5
- package/src/strip/guard.js +27 -28
- package/src/strip/js.js +27 -27
- package/src/strip.js +18 -21
- package/src/table.css +13 -14
- package/src/tokens.js +28 -27
- package/src/tool.js +10 -11
- package/templates/README.md +71 -77
- package/templates/ci.yml +33 -33
- package/templates/size-report.config.json +3 -3
- package/CHANGELOG.md +0 -690
package/src/page/app.js
CHANGED
|
@@ -2,20 +2,19 @@ import { appData, appUi, appView, appWrite, appNotice, appLinkUse, appRead, appA
|
|
|
2
2
|
import { appBody, appHead, appState } from './table.js';
|
|
3
3
|
import { appPanel } from './panel.js';
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
/* Assembling the table: what to show (the metrics and files the reader left on) and where to put it. The table
|
|
6
|
+
* chapter builds the markup of the head and the rows, the shared calculation gives the numbers — all that is
|
|
7
|
+
* left here is the decision and the insertion, with no numbers of its own. */
|
|
8
8
|
function appTable() {
|
|
9
9
|
const shown = appData.metrics.filter((m) => appView.metrics[m.key]);
|
|
10
10
|
const metrics = shown.map((m) => m.key);
|
|
11
11
|
const on = appView.files;
|
|
12
12
|
const files = [];
|
|
13
13
|
appData.files.forEach((f, i) => { if (on[i]) files.push(i); });
|
|
14
|
-
/*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* чисел: правка без изменения размера тоже правка. */
|
|
14
|
+
/* The columns the last commit touched come first: the report is rebuilt after every commit, and a reader's first
|
|
15
|
+
* question is what that edit brought. Inside each part the order stays as it comes from the settings — `sort` is
|
|
16
|
+
* stable, and the order of the columns is what the reader is used to. The mark comes from the history (which the
|
|
17
|
+
* engine knows) rather than from the numbers: an edit that changed no size is an edit too. */
|
|
19
18
|
files.sort((a, b) => (appData.last[a] === true ? 0 : 1) - (appData.last[b] === true ? 0 : 1));
|
|
20
19
|
|
|
21
20
|
const table = document.getElementById('grid');
|
|
@@ -31,12 +30,11 @@ function appTable() {
|
|
|
31
30
|
appWrite();
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
/*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* второго перечисления мест прокрутки в пакете нет. */
|
|
33
|
+
/* The panel's scroll is a property of the panel rather than of the markup, which is why it survives a rebuild:
|
|
34
|
+
* otherwise every click on a checkbox would send the list back to the top and the files at its end would be
|
|
35
|
+
* unreachable. Both the panel's scroll and the file list's are remembered — each has one of its own, and in a
|
|
36
|
+
* narrow window it is the list that scrolls. The elements are the ones the page really has (`#panel` from the
|
|
37
|
+
* markup, `.files` inside it from the panel): there is no second list of scroll places in the package. */
|
|
40
38
|
const appScrolled = ['#panel', '#panel .files'];
|
|
41
39
|
function appScrollTop() {
|
|
42
40
|
return appScrolled.map((sel) => {
|
|
@@ -52,12 +50,11 @@ function appScrollBack(saved) {
|
|
|
52
50
|
});
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
/*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* он возвращает клавиатуру, а не двигает список. */
|
|
53
|
+
/* The panel is redrawn whole, so the field under the keyboard and the scroll come back to their places after every
|
|
54
|
+
* rebuild: otherwise switching with Tab and Space would mean walking the panel from the start again, and the scroll
|
|
55
|
+
* would have to find its place anew. A field is identified by its ordinal number — the order of the panel's fields
|
|
56
|
+
* does not change between rebuilds. The focus is set without scrolling (`preventScroll`): it returns the keyboard
|
|
57
|
+
* rather than moving the list. */
|
|
61
58
|
function appRender(keepNotice) {
|
|
62
59
|
const at = Array.from(document.querySelectorAll('#panel input')).indexOf(document.activeElement);
|
|
63
60
|
const saved = appScrollTop();
|
|
@@ -65,16 +62,16 @@ function appRender(keepNotice) {
|
|
|
65
62
|
appScrollBack(saved);
|
|
66
63
|
if (at >= 0) document.querySelectorAll('#panel input')[at].focus({ preventScroll: true });
|
|
67
64
|
appTable();
|
|
68
|
-
/*
|
|
69
|
-
*
|
|
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. */
|
|
70
67
|
if (keepNotice !== true) appNotice('');
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
/*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
70
|
+
/* Restoring happens before the first drawing: for someone opening the page for the first time the view has to be the
|
|
71
|
+
* default rather than someone else's choice. A link outranks the memory: it is the sender's explicit choice, and
|
|
72
|
+
* while the reader has changed nothing it does not replace his own — writing it to the memory is what does not
|
|
73
|
+
* happen. A refused link is not an empty table but a message: the reader sees both what happened and what is shown
|
|
74
|
+
* instead. */
|
|
78
75
|
const appStart = appLinkUse();
|
|
79
76
|
if (appStart === 'ours') appTransient = true;
|
|
80
77
|
else if (appStart === 'refused') appForeign = true;
|
|
@@ -82,20 +79,18 @@ if (appStart !== 'ours') {
|
|
|
82
79
|
const appSaved = appRead();
|
|
83
80
|
if (appSaved !== null) appApply(appSaved);
|
|
84
81
|
}
|
|
85
|
-
/*
|
|
86
|
-
*
|
|
87
|
-
* заново на каждом заходе). */
|
|
82
|
+
/* The folded tree is the onlooker's memory rather than the reader's choice: it comes back even when someone else's
|
|
83
|
+
* link is open (otherwise a link sent over would unfold the tree again on every visit). */
|
|
88
84
|
appFoldRead();
|
|
89
85
|
appRender(true);
|
|
90
86
|
appStartup = false;
|
|
91
87
|
appForeign = false;
|
|
92
88
|
appTransient = false;
|
|
93
89
|
|
|
94
|
-
/*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* читателю, и до первого его действия это не наше. */
|
|
90
|
+
/* The anchor changed on an open page: the choice in the new address is applied by the same code as at opening. The
|
|
91
|
+
* page's own address raises no such event (`replaceState` does not), so there is no loop here. A refusal touches
|
|
92
|
+
* 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. */
|
|
99
94
|
window.addEventListener('hashchange', () => {
|
|
100
95
|
const state = appLinkUse();
|
|
101
96
|
if (state === 'refused') appForeign = true;
|
package/src/page/build.js
CHANGED
|
@@ -2,27 +2,25 @@ import fs from 'fs';
|
|
|
2
2
|
import { fill, LOCALES } from '../locales.js';
|
|
3
3
|
import { PAGE_CSS, TABLE_CSS } from '../css.js';
|
|
4
4
|
|
|
5
|
-
/*
|
|
6
|
-
*
|
|
5
|
+
/* Escaping text for markup lives here, because this builder is the only place that turns data into markup: the rest
|
|
6
|
+
* is drawn as nodes by the page. */
|
|
7
7
|
export function esc(s) {
|
|
8
8
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/*
|
|
12
|
-
*
|
|
13
|
-
* оформление страницы (`app.css`).
|
|
11
|
+
/* Building the report's page: data and program in one file, with no external reference. The styling comes as ordinary
|
|
12
|
+
* files too: the table's shared part (`table.css`) and the page's own (`app.css`).
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
* `src/derived.js`)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* The page's program is ordinary sources (the chapters in `src/page/*.js` and the shared calculation in
|
|
15
|
+
* `src/derived.js`) rather than strings inside the engine: a linter sees them, and the engine pastes them into the
|
|
16
|
+
* page. Module syntax is removed while pasting: in a browser that opened a file from disk there is nothing to resolve
|
|
17
|
+
* `import` with, and the declarations have to reach the shared scope in the order of pasting — the calculation first,
|
|
18
|
+
* then the chapters.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* главы снова стали одним файлом. */
|
|
20
|
+
* The chapters follow the page's subjects, and the order of pasting (the list below) is the order of declarations in
|
|
21
|
+
* the assembled program: the calculation first, then the choice's state, the nodes, the panel, the table and the
|
|
22
|
+
* assembling. The chapters are **slices of one text**: pasting glues them in a row, so the assembled page would stay
|
|
23
|
+
* the same if the chapters became one file again. */
|
|
26
24
|
export function stripModules(src) {
|
|
27
25
|
return src.split('\n')
|
|
28
26
|
.filter((line) => !/^import\s.*;\s*$/.test(line))
|
|
@@ -34,16 +32,16 @@ export function pageSource(file) {
|
|
|
34
32
|
return stripModules(fs.readFileSync(new URL(file, import.meta.url), 'utf8'));
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
/*
|
|
38
|
-
* (`test/page-view.test.js`
|
|
35
|
+
/* 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). */
|
|
39
37
|
export const PAGE_PARTS = ['./state.js', './dom.js', './panel.js', './table.js', './app.js'];
|
|
40
38
|
|
|
41
39
|
export function pageScript() {
|
|
42
40
|
return pageSource('../derived.js') + '\n' + PAGE_PARTS.map((part) => pageSource(part)).join('');
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
43
|
+
/* The note under the heading: what built the report and where it lies. The path is plain text rather than a link: the
|
|
44
|
+
* page opens from disk and depends on nothing. */
|
|
47
45
|
function subText(data, page) {
|
|
48
46
|
return fill(page.sub, {
|
|
49
47
|
tool: data.tool.name,
|
|
@@ -52,8 +50,8 @@ function subText(data, page) {
|
|
|
52
50
|
});
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
/*
|
|
56
|
-
*
|
|
53
|
+
/* The page's texts: column captions, panel labels, the legend and the empty states. They are the page's dictionary
|
|
54
|
+
* rather than the report's: the data block carries none of them, and the report's own words live in the locale. */
|
|
57
55
|
function uiText(page, loc) {
|
|
58
56
|
return {
|
|
59
57
|
commit: loc.commit,
|
|
@@ -69,29 +67,35 @@ function uiText(page, loc) {
|
|
|
69
67
|
linkForeign: page.linkForeign,
|
|
70
68
|
linkBroken: page.linkBroken,
|
|
71
69
|
linkExtra: page.linkExtra,
|
|
72
|
-
/*
|
|
73
|
-
*
|
|
70
|
+
/* Accuracy in two words: a metric's caption speaks about the worst in its column, while a cell's tooltip speaks
|
|
71
|
+
* about its own number. */
|
|
74
72
|
exact: page.exact,
|
|
75
73
|
approximate: page.approximate,
|
|
76
74
|
approxCell: page.approximateCell,
|
|
77
|
-
/*
|
|
78
|
-
*
|
|
75
|
+
/* Why a file is not in the report, in words: the engine names the reason with a mark (`why`), and the page dresses
|
|
76
|
+
* the mark in text, as it does with everything else in the panel. */
|
|
79
77
|
notMeasuredRule: page.notMeasuredRule,
|
|
80
78
|
notMeasuredChoice: page.notMeasuredChoice,
|
|
79
|
+
/* The tooltip of a file's checkbox: where the file stands and how its category was decided. The
|
|
80
|
+
* panel keeps no words of its own — a Russian report stays Russian in its chrome too, and an
|
|
81
|
+
* English one gets English there. */
|
|
82
|
+
notOnHead: page.notOnHead,
|
|
83
|
+
category: page.category,
|
|
84
|
+
categoryFromConfig: page.categoryFromConfig,
|
|
85
|
+
categoryByExtension: page.categoryByExtension,
|
|
81
86
|
methodLabel: page.panelMethod,
|
|
82
87
|
empty: page.emptyMetrics,
|
|
83
88
|
noFiles: page.noFiles,
|
|
84
|
-
/* {command}
|
|
89
|
+
/* {command} is substituted by the page, which holds the data, while {now} is filled in here. */
|
|
85
90
|
note: page.note.replace(/\{now\}/g, loc.now)
|
|
86
91
|
};
|
|
87
92
|
}
|
|
88
93
|
|
|
89
|
-
/*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* `--json` и `explain` отвечают этим же проходом. */
|
|
94
|
+
/* What does not go into the file. First, the list of skipped commits: it changes with the report's own commit (one
|
|
95
|
+
* with nothing to say lands in the list), and the file would stop being a **fixed point** — a rebuild after its own
|
|
96
|
+
* commit would yield different bytes and the hook would commit the report forever. The page has no use for the list
|
|
97
|
+
* at all: it does not show it. It stays available to the reader — `--data`, `--json` and `explain` answer from the
|
|
98
|
+
* same run. */
|
|
95
99
|
const NOT_IN_FILE = ['skipped'];
|
|
96
100
|
|
|
97
101
|
export function pagePayload(data) {
|
|
@@ -100,10 +104,8 @@ export function pagePayload(data) {
|
|
|
100
104
|
return out;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/*
|
|
104
|
-
*
|
|
105
|
-
* `<` в данных экранируется: иначе подпись коммита или путь закрыли бы тег
|
|
106
|
-
* раньше времени (в JSON такой экранированный символ читается как обычный). */
|
|
107
|
+
/* The report's page is one file: the data lies in it, the script is pasted in, there are no external references. Hence
|
|
108
|
+
* it opens with a double click and works without a network. */
|
|
107
109
|
export function pageHtml(data, cfg) {
|
|
108
110
|
const loc = LOCALES[cfg.locale];
|
|
109
111
|
return '<!doctype html>\n<html lang="' + esc(loc.html) + '">\n<head>\n<meta charset="utf-8">\n'
|
|
@@ -122,9 +124,8 @@ export function pageHtml(data, cfg) {
|
|
|
122
124
|
+ '<script>\n' + pageScript() + '</script>\n</body>\n</html>\n';
|
|
123
125
|
}
|
|
124
126
|
|
|
125
|
-
/* JSON
|
|
126
|
-
*
|
|
127
|
-
* обычный). */
|
|
127
|
+
/* JSON inside the page: `<` is escaped, or a commit's subject or a path would close the tag early (inside a JSON
|
|
128
|
+
* string such an escaped character reads as a most ordinary one). */
|
|
128
129
|
function jsonInHtml(value) {
|
|
129
130
|
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
130
131
|
}
|
package/src/page/dom.js
CHANGED
|
@@ -6,9 +6,8 @@ export function appEl(tag, cls, text) {
|
|
|
6
6
|
return el;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/*
|
|
10
|
-
*
|
|
11
|
-
* технологии. Подпись видимая, подробности — во всплывающей строке. */
|
|
9
|
+
/* A switch is a label around an input: one click target, which is why a mouse, the keyboard (`Space` on the input)
|
|
10
|
+
* and assistive technology all reach it. The label is visible, the details live in the tooltip. */
|
|
12
11
|
export function appBox(label, title, checked, onChange, cls) {
|
|
13
12
|
const box = appEl('label', 'box' + (cls ? ' ' + cls : ''));
|
|
14
13
|
const input = document.createElement('input');
|
|
@@ -21,16 +20,16 @@ export function appBox(label, title, checked, onChange, cls) {
|
|
|
21
20
|
return box;
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
/*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
/* A checkbox with nothing to switch: the place in the tree exists while there is nothing to switch on — the file is
|
|
24
|
+
* not among the columns and the report does not measure it. The checkbox is off and unavailable: that way the row
|
|
25
|
+
* looks like every other one (the eye compares like with like) while showing that this is not "switched off by the
|
|
26
|
+
* reader" but "not measured". The reason lives in the tooltip. */
|
|
28
27
|
export function appOffBox(label, title, cls) {
|
|
29
28
|
const box = appBox(label, title, false, null, cls);
|
|
30
29
|
box.classList.add('plain');
|
|
31
30
|
box.querySelector('input').disabled = true;
|
|
32
|
-
/*
|
|
33
|
-
*
|
|
31
|
+
/* The tooltip goes on the whole row rather than the input alone: a browser shows none for a disabled input, while
|
|
32
|
+
* the reader needs the reason right here. */
|
|
34
33
|
box.title = title;
|
|
35
34
|
return box;
|
|
36
35
|
}
|
package/src/page/panel.js
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
1
|
import { appEl, appBox, appOffBox } from './dom.js';
|
|
2
2
|
import { appData, appUi, appView, appFileAt, appFoldSet, appMeasured } from './state.js';
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
/* Only a file sets a file's checkbox: both a category and a folder in the tree are ways to set the same checkboxes as
|
|
5
|
+
* a group and keep no state of their own. Otherwise one and the same decision would live in two places and drift
|
|
6
|
+
* apart. */
|
|
7
7
|
function appFileBox(i) {
|
|
8
8
|
const f = appData.files[i];
|
|
9
|
-
const where = appFileAt(i) + (f.path === null ?
|
|
10
|
-
return appBox(f.label, where +
|
|
11
|
-
+ (f.categoryBy === 'config' ?
|
|
9
|
+
const where = appFileAt(i) + (f.path === null ? appUi.notOnHead : '');
|
|
10
|
+
return appBox(f.label, where + appUi.category
|
|
11
|
+
+ (f.categoryBy === 'config' ? appUi.categoryFromConfig : appUi.categoryByExtension),
|
|
12
|
+
appView.files[i], (e) => {
|
|
12
13
|
appView.files[i] = e.target.checked;
|
|
13
14
|
appRender();
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
/*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
/* A file that is not in the report: it stands in its place in the tree with its checkbox off and unavailable — no
|
|
19
|
+
* numbers were measured for it, so there is nothing to switch. The reader sees the reason in the tooltip rather than
|
|
20
|
+
* guessing it from the look. */
|
|
20
21
|
function appUnmeasuredBox(entry) {
|
|
21
22
|
return appOffBox(entry.path.split('/').pop(), entry.path + ' · '
|
|
22
23
|
+ (entry.why === 'rule' ? appUi.notMeasuredRule : appUi.notMeasuredChoice));
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
/*
|
|
26
|
-
*
|
|
26
|
+
/* Every measured file of a subtree — what a folder's switch controls: a file outside the report has nothing to
|
|
27
|
+
* switch. */
|
|
27
28
|
function appIndexes(node) {
|
|
28
29
|
const out = node.files.slice();
|
|
29
30
|
node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
|
|
30
31
|
return out;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/*
|
|
34
|
+
/* How many files a subtree holds — including the ones that made it into no report. */
|
|
34
35
|
function appCount(node) {
|
|
35
36
|
let n = node.files.length + node.others.length;
|
|
36
37
|
node.dirs.forEach((sub) => { n += appCount(sub); });
|
|
37
38
|
return n;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/*
|
|
41
|
+
/* A tree node: the measured files (columns), the project's other files and the subfolders. */
|
|
41
42
|
function appNode() {
|
|
42
43
|
return { files: [], others: [], dirs: new Map() };
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
/*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
/* A folder's switch: its checkbox carries the whole subtree with it and shows three states — every file on, some,
|
|
47
|
+
* none. The number of files stands next to it; when the folder also holds files outside the report, it is written as
|
|
48
|
+
* a fraction ("2/5"): what matters to the reader is that the folder holds five files while two are measured. A folder
|
|
49
|
+
* without a single measured file stays in place with its checkbox off and unavailable: there is nothing to switch on
|
|
50
|
+
* in it. */
|
|
50
51
|
function appDirHead(name, sub) {
|
|
51
52
|
const idx = appIndexes(sub);
|
|
52
53
|
const total = appCount(sub);
|
|
@@ -68,18 +69,16 @@ function appDirHead(name, sub) {
|
|
|
68
69
|
return head;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
/*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* же нажатие.
|
|
72
|
+
/* The folder's sign is a click target of its own, separate from the checkbox: the checkbox answers for the numbers (it
|
|
73
|
+
* switches the subtree's files on), while the sign answers for how much of the tree is visible. One target for two
|
|
74
|
+
* different decisions would mean a folder can be folded only together with switching its files on. The sign is drawn
|
|
75
|
+
* 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
|
+
* control nested in it would be reached as that same target.
|
|
77
77
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* вещи, которые читатель и видит: класс, знак и запись в памяти. */
|
|
78
|
+
* 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
|
+
* would be honest work for nothing — it counts the whole table (every row by every column) and so pays for numbers
|
|
80
|
+
* folding does not change. That is why only the three things the reader sees change: the class, the sign and the note
|
|
81
|
+
* in the memory. */
|
|
83
82
|
function appFoldBox(name, path) {
|
|
84
83
|
const folded = appView.folded[path] === true;
|
|
85
84
|
const box = appEl('span', 'fold', folded ? '▸' : '▾');
|
|
@@ -95,8 +94,8 @@ function appFoldBox(name, path) {
|
|
|
95
94
|
return box;
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
/*
|
|
99
|
-
*
|
|
97
|
+
/* The leaves of one level: measured files and files outside the report mixed together and sorted by name, the way a
|
|
98
|
+
* file tree reads, rather than as separate lists. */
|
|
100
99
|
function appLeaves(node) {
|
|
101
100
|
const items = node.files.map((i) => ({ name: appData.files[i].label, i: i, entry: null }));
|
|
102
101
|
node.others.forEach((entry) => {
|
|
@@ -105,8 +104,8 @@ function appLeaves(node) {
|
|
|
105
104
|
return items.sort((a, b) => (a.name < b.name ? -1 : (a.name > b.name ? 1 : 0)));
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
/*
|
|
109
|
-
*
|
|
107
|
+
/* A folder row: the folding sign, the checkbox with the number of files and the subtree. A folded folder differs by
|
|
108
|
+
* its class alone — the markup stays the same. */
|
|
110
109
|
function appDir(name, sub, prefix) {
|
|
111
110
|
const here = prefix === '' ? name : prefix + '/' + name;
|
|
112
111
|
const folded = appView.folded[here] === true;
|
|
@@ -117,18 +116,17 @@ function appDir(name, sub, prefix) {
|
|
|
117
116
|
return li;
|
|
118
117
|
}
|
|
119
118
|
|
|
120
|
-
//
|
|
119
|
+
// A leaf row: a measured file comes with a checkbox, a file outside the report with one switched off.
|
|
121
120
|
function appLeaf(leaf) {
|
|
122
121
|
const li = appEl('li');
|
|
123
122
|
li.appendChild(leaf.entry === null ? appFileBox(leaf.i) : appUnmeasuredBox(leaf.entry));
|
|
124
123
|
return li;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
/*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* а найти его по-прежнему можно — оно там же, где было. */
|
|
126
|
+
/* The nodes of one level: first everything in the report (folders alphabetically, then the leaves in the order
|
|
127
|
+
* `appLeaves` gives), then what is not in it — folders without a measured file and files outside the columns. That is
|
|
128
|
+
* not a matter of taste: everything outside the report has its checkbox off and unavailable, so at the end of the list
|
|
129
|
+
* it does not distract from what is in the table, while it can still be found — in the same place where it was. */
|
|
132
130
|
function appTreeList(node, prefix) {
|
|
133
131
|
const list = appEl('ul', 'tree');
|
|
134
132
|
const dirs = [...node.dirs.keys()].sort()
|
|
@@ -142,8 +140,8 @@ function appTreeList(node, prefix) {
|
|
|
142
140
|
return list;
|
|
143
141
|
}
|
|
144
142
|
|
|
145
|
-
/*
|
|
146
|
-
*
|
|
143
|
+
/* A leaf's place in the tree: the path is split on "/" and the intermediate folders are created on the way. One place
|
|
144
|
+
* for measured and other files alike — otherwise they would drift apart in folders. */
|
|
147
145
|
function appLeafAt(node, p, i, entry) {
|
|
148
146
|
const parts = p.split('/');
|
|
149
147
|
for (let d = 0; d < parts.length - 1; d++) {
|
|
@@ -154,11 +152,10 @@ function appLeafAt(node, p, i, entry) {
|
|
|
154
152
|
else node.others.push(entry);
|
|
155
153
|
}
|
|
156
154
|
|
|
157
|
-
/*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* пути. */
|
|
155
|
+
/* The page's tree is the project's tree: the nodes come from the catalogue (every path git sees), which is why it also
|
|
156
|
+
* holds files outside the report. A measured leaf is a column, and its path is the one in the file's caption; a column
|
|
157
|
+
* whose file is gone from HEAD never entered the catalogue (the index does not hold it) and stands in place by the last
|
|
158
|
+
* path known. */
|
|
162
159
|
function appTree() {
|
|
163
160
|
const root = appNode();
|
|
164
161
|
appData.files.forEach((_f, i) => appLeafAt(root, appFileAt(i), i, null));
|
|
@@ -183,9 +180,9 @@ export function appPanel() {
|
|
|
183
180
|
}, 'metric'));
|
|
184
181
|
});
|
|
185
182
|
metrics.appendChild(mrow);
|
|
186
|
-
/*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
183
|
+
/* What produced each number is visible rather than hidden in a tooltip: the token dictionary and the way of
|
|
184
|
+
* compression are chosen by the settings of the run, the page has nothing to switch them with, and the reader needs
|
|
185
|
+
* to know this without pointing a mouse. */
|
|
189
186
|
appData.metrics.forEach((m) => {
|
|
190
187
|
metrics.appendChild(appEl('p', 'about', m.label + ' — ' + appUi.methodLabel + ' ' + m.method));
|
|
191
188
|
});
|
|
@@ -193,8 +190,8 @@ export function appPanel() {
|
|
|
193
190
|
|
|
194
191
|
const files = appEl('fieldset', 'files');
|
|
195
192
|
files.appendChild(appEl('legend', null, appUi.files));
|
|
196
|
-
/*
|
|
197
|
-
*
|
|
193
|
+
/* The row of categories is marked by a class: the file list scrolls, and the row stays in sight (its stickiness lives
|
|
194
|
+
* in the wide layout, which is where the panel scrolls). */
|
|
198
195
|
const cats = appEl('div', 'row cats');
|
|
199
196
|
appData.categories.forEach((cat) => {
|
|
200
197
|
const idx = [];
|