@vernikr/size-report 1.2.0 → 1.3.1
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/CHANGELOG.md +91 -0
- package/README.md +183 -71
- package/package.json +21 -2
- package/src/args.js +191 -0
- package/src/artifact.js +9 -2
- package/src/check.js +8 -10
- package/src/cli.js +47 -457
- package/src/config.js +103 -24
- package/src/data.js +16 -9
- package/src/derived.js +3 -2
- package/src/doctor.js +121 -65
- package/src/explain.js +43 -26
- package/src/history.js +94 -77
- package/src/hook.js +17 -11
- package/src/init.js +75 -0
- package/src/modes.js +190 -0
- package/src/page/app.css +30 -0
- package/src/page/app.js +9 -498
- package/src/page/build.js +16 -7
- package/src/page/dom.js +22 -0
- package/src/page/panel.js +124 -0
- package/src/page/state.js +226 -0
- package/src/page/table.js +145 -0
- package/src/project.js +262 -0
- package/src/refusal.js +6 -1
- package/src/render.js +19 -11
- package/src/size-table.js +10 -4
- package/src/strip/forms.js +26 -0
- package/src/strip/guard.js +59 -0
- package/src/strip/js.js +136 -0
- package/src/strip.js +17 -190
- package/templates/README.md +16 -10
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { appEl, appBox } from './dom.js';
|
|
2
|
+
import { appData, appUi, appView, appFileAt } from './state.js';
|
|
3
|
+
|
|
4
|
+
/* Легенда: образцы — теми же классами, что и числа в клетках (`up`/`down` из
|
|
5
|
+
* общей части оформления), поэтому образец не может разойтись с цветом числа. */
|
|
6
|
+
function appLegend() {
|
|
7
|
+
const list = appEl('ul', 'legend');
|
|
8
|
+
appUi.legend.forEach((item) => {
|
|
9
|
+
const li = appEl('li');
|
|
10
|
+
li.appendChild(appEl('span', 'swatch ' + item.cls));
|
|
11
|
+
li.appendChild(appEl('span', null, item.text));
|
|
12
|
+
list.appendChild(li);
|
|
13
|
+
});
|
|
14
|
+
return list;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Галочку файла ставит только файл: и категория, и папка в дереве — способы
|
|
18
|
+
* переставить те же галочки сразу группой, а своего состояния у них нет. Иначе
|
|
19
|
+
* одно и то же решение жило бы в двух местах и расходилось. */
|
|
20
|
+
function appFileBox(i) {
|
|
21
|
+
const f = appData.files[i];
|
|
22
|
+
const where = appFileAt(i) + (f.path === null ? ' (нет на HEAD)' : '');
|
|
23
|
+
return appBox(f.label, where + ' · категория: '
|
|
24
|
+
+ (f.categoryBy === 'config' ? 'из настроек' : 'по расширению'), appView.files[i], (e) => {
|
|
25
|
+
appView.files[i] = e.target.checked;
|
|
26
|
+
appRender();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Все файлы поддерева — то, чем управляет переключатель папки. */
|
|
31
|
+
function appIndexes(node) {
|
|
32
|
+
const out = node.files.slice();
|
|
33
|
+
node.dirs.forEach((sub) => { out.push(...appIndexes(sub)); });
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Узлы одного уровня: сперва папки по алфавиту, затем файлы в порядке данных.
|
|
38
|
+
* Переключатель папки стоит над своим поддеревом и показывает три состояния: все
|
|
39
|
+
* файлы включены, часть, ни одного. */
|
|
40
|
+
function appTreeList(node) {
|
|
41
|
+
const list = appEl('ul', 'tree');
|
|
42
|
+
[...node.dirs.keys()].sort().forEach((name) => {
|
|
43
|
+
const sub = node.dirs.get(name);
|
|
44
|
+
const idx = appIndexes(sub);
|
|
45
|
+
const on = idx.map((i) => appView.files[i]);
|
|
46
|
+
const some = on.some((v) => v);
|
|
47
|
+
const head = appBox(name + '/', appUi.dir.replace('{name}', name).replace('{n}', idx.length),
|
|
48
|
+
some && on.every((v) => v), (e) => {
|
|
49
|
+
idx.forEach((i) => { appView.files[i] = e.target.checked; });
|
|
50
|
+
appRender();
|
|
51
|
+
}, 'dir');
|
|
52
|
+
head.querySelector('input').indeterminate = some && !on.every((v) => v);
|
|
53
|
+
head.appendChild(appEl('span', 'n', idx.length));
|
|
54
|
+
const li = appEl('li');
|
|
55
|
+
li.appendChild(head);
|
|
56
|
+
li.appendChild(appTreeList(sub));
|
|
57
|
+
list.appendChild(li);
|
|
58
|
+
});
|
|
59
|
+
node.files.forEach((i) => {
|
|
60
|
+
const li = appEl('li');
|
|
61
|
+
li.appendChild(appFileBox(i));
|
|
62
|
+
list.appendChild(li);
|
|
63
|
+
});
|
|
64
|
+
return list;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Дерево файлов: путь делится по «/», папки становятся узлами, файлы — листьями.
|
|
68
|
+
* Строится из тех же путей, что показаны в подписи файла, поэтому дерево и список
|
|
69
|
+
* файлов не могут разойтись. */
|
|
70
|
+
function appTree() {
|
|
71
|
+
const root = { files: [], dirs: new Map() };
|
|
72
|
+
appData.files.forEach((f, i) => {
|
|
73
|
+
const parts = (f.path === null ? f.paths[0] : f.path).split('/');
|
|
74
|
+
let node = root;
|
|
75
|
+
for (let d = 0; d < parts.length - 1; d++) {
|
|
76
|
+
if (!node.dirs.has(parts[d])) node.dirs.set(parts[d], { files: [], dirs: new Map() });
|
|
77
|
+
node = node.dirs.get(parts[d]);
|
|
78
|
+
}
|
|
79
|
+
node.files.push(i);
|
|
80
|
+
});
|
|
81
|
+
return appTreeList(root);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function appPanel() {
|
|
85
|
+
const panel = document.getElementById('panel');
|
|
86
|
+
panel.textContent = '';
|
|
87
|
+
|
|
88
|
+
const metrics = appEl('fieldset');
|
|
89
|
+
metrics.appendChild(appEl('legend', null, appUi.metrics));
|
|
90
|
+
const mrow = appEl('div', 'row');
|
|
91
|
+
appData.metrics.forEach((m) => {
|
|
92
|
+
const word = m.accuracy === 'exact' ? appUi.exact : appUi.approximate;
|
|
93
|
+
mrow.appendChild(appBox(m.label, m.note + ' · ' + word, appView.metrics[m.key], (e) => {
|
|
94
|
+
appView.metrics[m.key] = e.target.checked;
|
|
95
|
+
appRender();
|
|
96
|
+
}, 'metric'));
|
|
97
|
+
});
|
|
98
|
+
metrics.appendChild(mrow);
|
|
99
|
+
/* Чем получено каждое число — видно, а не только во всплывающей строке: словарь
|
|
100
|
+
* токенов и способ сжатия выбираются настройками запуска, переключить их
|
|
101
|
+
* странице нечем, и читателю важно знать это, не наводя мышь. */
|
|
102
|
+
appData.metrics.forEach((m) => {
|
|
103
|
+
metrics.appendChild(appEl('p', 'about', m.label + ' — ' + appUi.methodLabel + ' ' + m.method));
|
|
104
|
+
});
|
|
105
|
+
panel.appendChild(metrics);
|
|
106
|
+
|
|
107
|
+
const files = appEl('fieldset', 'files');
|
|
108
|
+
files.appendChild(appEl('legend', null, appUi.files));
|
|
109
|
+
const cats = appEl('div', 'row');
|
|
110
|
+
appData.categories.forEach((cat) => {
|
|
111
|
+
const idx = [];
|
|
112
|
+
appData.files.forEach((f, i) => { if (f.category === cat.key) idx.push(i); });
|
|
113
|
+
cats.appendChild(appBox(cat.label, appUi.all + ' · ' + cat.label, idx.every((i) => appView.files[i]),
|
|
114
|
+
(e) => {
|
|
115
|
+
idx.forEach((i) => { appView.files[i] = e.target.checked; });
|
|
116
|
+
appRender();
|
|
117
|
+
}, 'all'));
|
|
118
|
+
});
|
|
119
|
+
files.appendChild(cats);
|
|
120
|
+
files.appendChild(appTree());
|
|
121
|
+
panel.appendChild(files);
|
|
122
|
+
|
|
123
|
+
panel.appendChild(appLegend());
|
|
124
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/* Программа страницы отчёта: панель выбора с легендой и таблица.
|
|
2
|
+
*
|
|
3
|
+
* Это обычный исходник, а не строка в движке: его видит линтер, и он же
|
|
4
|
+
* вклеивается в собранную страницу (`pageScript` снимает модульный синтаксис —
|
|
5
|
+
* импорт ниже и объявления с `export` из соседнего файла). Расчёт берётся из
|
|
6
|
+
* производных величин, поэтому включение метрики, категории или файла зовёт тот
|
|
7
|
+
* же расчёт, что считает статическую таблицу, и не может дать других чисел.
|
|
8
|
+
*
|
|
9
|
+
* Страница — один файл без внешних ссылок, поэтому здесь нет ни динамического
|
|
10
|
+
* импорта, ни загрузки чего-либо по сети: оформление приходит тем же файлом,
|
|
11
|
+
* а разметка клеток повторяет статическую таблицу (`clip` и подпись коммита —
|
|
12
|
+
* правила общей части оформления).
|
|
13
|
+
*
|
|
14
|
+
* Панель помнит выбор читателя между открытиями и умеет передать его ссылкой
|
|
15
|
+
* («Память выбора» ниже): запись привязана к паспорту отчёта и хранит только
|
|
16
|
+
* выключенное по именам, поэтому чужая запись не применяется, а исчезнувшее имя
|
|
17
|
+
* просто ничего не значит. Та же запись ложится в адрес — его и отправляют коллеге.
|
|
18
|
+
*
|
|
19
|
+
* Точность числа страница не выводит сама: пометки приближённых клеток приходят в
|
|
20
|
+
* данных, от того же правила, по которому названа точность метрики. Из путей и
|
|
21
|
+
* форматов страница такого вывода не делает — второго правила точности не будет. */
|
|
22
|
+
|
|
23
|
+
/* Импорт — одной строкой: модульный синтаксис снимается при вклейке построчно,
|
|
24
|
+
* и оставшаяся строка `import` попала бы в страницу (её ловит проверка). */
|
|
25
|
+
|
|
26
|
+
export const appData = JSON.parse(document.getElementById('data').textContent);
|
|
27
|
+
export const appUi = JSON.parse(document.getElementById('ui').textContent);
|
|
28
|
+
export const appView = { metrics: {}, files: [] };
|
|
29
|
+
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
|
|
30
|
+
appData.files.forEach(() => { appView.files.push(true); });
|
|
31
|
+
|
|
32
|
+
/* Описание метрики по ключу: подсказка приближённой клетки называет способ её
|
|
33
|
+
* числа — тот же, что стоит в подписи метрики, поэтому двух ответов про «чем
|
|
34
|
+
* посчитано» у страницы нет. */
|
|
35
|
+
export const appMetric = {};
|
|
36
|
+
appData.metrics.forEach((m) => { appMetric[m.key] = m; });
|
|
37
|
+
|
|
38
|
+
/* Ссылка — это тот же выбор в адресе, под своим именем: чужой якорь страницы
|
|
39
|
+
* ссылкой не считается, и спорить с ним нечем. */
|
|
40
|
+
const APP_LINK = '#size-report=';
|
|
41
|
+
|
|
42
|
+
/* Три обстоятельства первой отрисовки, которые действуют только на ней:
|
|
43
|
+
* адрес в ней не переписывается (его прислали читателю, а не наоборот), память
|
|
44
|
+
* не трогается (присланная ссылка — не выбор читателя), а сообщение о ссылке
|
|
45
|
+
* ещё не гаснет. */
|
|
46
|
+
let appStartup = true;
|
|
47
|
+
let appForeign = false;
|
|
48
|
+
let appTransient = false;
|
|
49
|
+
|
|
50
|
+
/* -------- память выбора читателя -------- */
|
|
51
|
+
|
|
52
|
+
/* Имя файла для записи — путь на HEAD, а если файла там уже нет, последний из
|
|
53
|
+
* настроек: по нему файл и опознаётся в отчёте. */
|
|
54
|
+
export function appFileAt(i) {
|
|
55
|
+
const f = appData.files[i];
|
|
56
|
+
return f.path === null ? f.paths[0] : f.path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Отпечаток паспорта: опознавательный знак записи, а не защита от подделки,
|
|
60
|
+
* поэтому 32 бит достаточно (FNV-1a). */
|
|
61
|
+
function appHash(text) {
|
|
62
|
+
let h = 2166136261;
|
|
63
|
+
for (let i = 0; i < text.length; i++) {
|
|
64
|
+
h ^= text.charCodeAt(i);
|
|
65
|
+
h = Math.imul(h, 16777619);
|
|
66
|
+
}
|
|
67
|
+
return (h >>> 0).toString(16);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Паспорт отчёта: имя инструмента, схема данных, путь артефакта, заголовок и метки
|
|
71
|
+
* колонок в порядке отчёта. Он и отделяет один отчёт от другого — по нему выбирается
|
|
72
|
+
* ключ записи, поэтому выбор с чужого отчёта не подхватывается. Версии пакета и
|
|
73
|
+
* верхушки истории в паспорте нет намеренно: это тот же отчёт — обновление
|
|
74
|
+
* инструмента не меняет того, что значит колонка, а подросшая история это та же
|
|
75
|
+
* история, к которой читатель и возвращается. */
|
|
76
|
+
function appPassport() {
|
|
77
|
+
return appHash([appData.tool.name, appData.schema, appData.report.artifact,
|
|
78
|
+
appData.report.title, appData.files.map((f) => f.label).join('|')].join('\n'));
|
|
79
|
+
}
|
|
80
|
+
const appKey = 'size-report:' + appPassport();
|
|
81
|
+
|
|
82
|
+
/* Запись выбора — одна на всё: её кладут и в память, и в адрес, поэтому двух
|
|
83
|
+
* форматов одного состояния не бывает. Хранится только выключенное, по именам:
|
|
84
|
+
* «включено» и «записи нет» — одно и то же состояние, поэтому возврат всех галочек
|
|
85
|
+
* убирает запись, а не оставляет след, неотличимый от выбора. */
|
|
86
|
+
function appRecord() {
|
|
87
|
+
const metrics = {};
|
|
88
|
+
const files = {};
|
|
89
|
+
appData.metrics.forEach((m) => { if (!appView.metrics[m.key]) metrics[m.key] = false; });
|
|
90
|
+
appData.files.forEach((_f, i) => { if (!appView.files[i]) files[appFileAt(i)] = false; });
|
|
91
|
+
return { v: 1, passport: appPassport(), metrics: metrics, files: files };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Своя ли запись и того ли формата — одно правило и для памяти, и для адреса.
|
|
95
|
+
function appRecordOk(rec) {
|
|
96
|
+
return rec !== null && typeof rec === 'object' && rec.v === 1 && rec.passport === appPassport();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function appWrite() {
|
|
100
|
+
const rec = appRecord();
|
|
101
|
+
const empty = Object.keys(rec.metrics).length === 0 && Object.keys(rec.files).length === 0;
|
|
102
|
+
if (!appTransient) {
|
|
103
|
+
try {
|
|
104
|
+
if (empty) window.localStorage.removeItem(appKey);
|
|
105
|
+
else window.localStorage.setItem(appKey, JSON.stringify(rec));
|
|
106
|
+
} catch (_e) {
|
|
107
|
+
/* Памяти нет (браузер её не даёт этой странице): выбор не переживёт закрытия,
|
|
108
|
+
* а числа и разметка от этого не зависят. */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* Адрес и есть ссылка для коллеги, поэтому он повторяет выбор. Но не на первой
|
|
112
|
+
* отрисовке и не тогда, когда ссылка оказалась чужой: присланный адрес — не наш,
|
|
113
|
+
* его читателю ещё читать. */
|
|
114
|
+
if (appStartup || appForeign) return;
|
|
115
|
+
try {
|
|
116
|
+
window.history.replaceState(null, '', APP_LINK + encodeURIComponent(JSON.stringify(rec)));
|
|
117
|
+
} catch (_e) {
|
|
118
|
+
/* Браузер не даёт менять адрес: ссылку тогда берут из памяти браузера. */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Сброс к «включено всё»: граница между «в отчёте этого больше нет» и
|
|
123
|
+
* «выключено» — это запись, а не отсутствие значения. Ссылка несёт весь выбор
|
|
124
|
+
* отправителя, поэтому применяется на чистом виде, а не поверх чужого. */
|
|
125
|
+
function appAll() {
|
|
126
|
+
appData.metrics.forEach((m) => { appView.metrics[m.key] = true; });
|
|
127
|
+
appData.files.forEach((_f, i) => { appView.files[i] = true; });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Что говорит адрес. Отвечает либо своей записью, либо отказом (`linkForeign` —
|
|
131
|
+
* ссылка другого отчёта, `linkBroken` — прочитать нечего): чужой или испорченный
|
|
132
|
+
* выбор не применяется, но и не молчит — иначе читатель не поймёт, почему он видит
|
|
133
|
+
* не то, что ему прислали. Адрес без имени ссылки не ссылка вовсе: молчание, чтобы
|
|
134
|
+
* не спорить с обычными якорями страницы. */
|
|
135
|
+
function appLinkRead() {
|
|
136
|
+
const hash = window.location.hash || '';
|
|
137
|
+
if (hash.indexOf(APP_LINK) !== 0) return { rec: null, refused: null };
|
|
138
|
+
const body = hash.slice(APP_LINK.length);
|
|
139
|
+
let rec = null;
|
|
140
|
+
try {
|
|
141
|
+
rec = JSON.parse(decodeURIComponent(body));
|
|
142
|
+
} catch (_e) {
|
|
143
|
+
try {
|
|
144
|
+
rec = JSON.parse(body);
|
|
145
|
+
} catch (_e2) {
|
|
146
|
+
return { rec: null, refused: 'linkBroken' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (!appRecordOk(rec)) {
|
|
150
|
+
return { rec: null, refused: rec === null || typeof rec !== 'object' || rec.v !== 1
|
|
151
|
+
? 'linkBroken' : 'linkForeign' };
|
|
152
|
+
}
|
|
153
|
+
return { rec: rec, refused: null, extra: appUnknown(rec) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Сколько имён в ссылке этому отчёту неизвестны: о них читателю надо сказать —
|
|
157
|
+
* иначе он будет искать в таблице то, чего в ней и не было. */
|
|
158
|
+
function appUnknown(rec) {
|
|
159
|
+
const known = {};
|
|
160
|
+
const metricKeys = {};
|
|
161
|
+
appData.files.forEach((_f, i) => { known[appFileAt(i)] = true; });
|
|
162
|
+
appData.metrics.forEach((m) => { metricKeys[m.key] = true; });
|
|
163
|
+
let n = 0;
|
|
164
|
+
Object.keys(rec.metrics || {}).forEach((k) => { if (!metricKeys[k]) n++; });
|
|
165
|
+
Object.keys(rec.files || {}).forEach((k) => { if (!known[k]) n++; });
|
|
166
|
+
return n;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Сообщение о ссылке гаснет после первого же действия читателя: он его прочитал, а
|
|
170
|
+
* постоянное предупреждение — это шум поверх чисел. */
|
|
171
|
+
export function appNotice(text) {
|
|
172
|
+
const el = document.getElementById('notice');
|
|
173
|
+
el.textContent = text;
|
|
174
|
+
el.hidden = text === '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Что делает с адресом его событие — открытие страницы и перемена якоря на уже
|
|
178
|
+
* открытой (браузер в этом случае документ не перезагружает, а лишь переставляет
|
|
179
|
+
* якорь, поэтому без этого разбора ссылка работала бы только в новой вкладке).
|
|
180
|
+
* Своя ссылка заменяет вид целиком: в ней весь выбор отправителя, а не разница с
|
|
181
|
+
* чужим. Отказ объясняется словами — и не трогает ни вид, ни адрес. */
|
|
182
|
+
export function appLinkUse() {
|
|
183
|
+
const link = appLinkRead();
|
|
184
|
+
if (link.rec !== null) {
|
|
185
|
+
appAll();
|
|
186
|
+
appApply(link.rec);
|
|
187
|
+
if (link.extra > 0) appNotice(appUi.linkExtra.replace('{n}', link.extra));
|
|
188
|
+
return 'ours';
|
|
189
|
+
}
|
|
190
|
+
if (link.refused !== null) {
|
|
191
|
+
appNotice(appUi[link.refused]);
|
|
192
|
+
return 'refused';
|
|
193
|
+
}
|
|
194
|
+
return 'none';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Чтение: только своя запись — своей версии формата и своего паспорта. Запись
|
|
198
|
+
* чужого отчёта лежит под другим ключом, а чужая, устаревшая или испорченная
|
|
199
|
+
* равносильна её отсутствию. */
|
|
200
|
+
export function appRead() {
|
|
201
|
+
let text = null;
|
|
202
|
+
try {
|
|
203
|
+
text = window.localStorage.getItem(appKey);
|
|
204
|
+
} catch (_e) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (text === null) return null;
|
|
208
|
+
let rec = null;
|
|
209
|
+
try {
|
|
210
|
+
rec = JSON.parse(text);
|
|
211
|
+
} catch (_e) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return appRecordOk(rec) ? rec : null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Применение — по именам: файл опознаётся путём, метрика ключом. Имени, которого в
|
|
218
|
+
* отчёте нет, ничего не соответствует (колонку перенаправили на другой путь,
|
|
219
|
+
* метрику убрали из настроек), а появившиеся файлы и метрики остаются включёнными —
|
|
220
|
+
* как их видит тот, кто открыл страницу впервые. */
|
|
221
|
+
export function appApply(rec) {
|
|
222
|
+
const metrics = rec.metrics || {};
|
|
223
|
+
const files = rec.files || {};
|
|
224
|
+
appData.metrics.forEach((m) => { if (metrics[m.key] === false) appView.metrics[m.key] = false; });
|
|
225
|
+
appData.files.forEach((_f, i) => { if (files[appFileAt(i)] === false) appView.files[i] = false; });
|
|
226
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { cellParts, commitParts, nowModel, rowModel, valueParts } from '../derived.js';
|
|
2
|
+
import { appEl } from './dom.js';
|
|
3
|
+
import { appData, appUi, appView, appMetric } from './state.js';
|
|
4
|
+
|
|
5
|
+
/* Приближённая клетка: пометка берётся из данных движка, а не выводится здесь из
|
|
6
|
+
* пути файла, — правило точности живёт там же, где считаются числа. У клетки
|
|
7
|
+
* файла это её собственное число, у итога — худшее из вошедших в него, иначе
|
|
8
|
+
* сумма обещала бы точность, которой нет у слагаемых. Возвращается описание
|
|
9
|
+
* метрики (её способ и идёт в подсказку клетки) либо ничего. */
|
|
10
|
+
export function appApprox(where, files, key) {
|
|
11
|
+
const marks = appData.approx[key];
|
|
12
|
+
if (marks === undefined) return null;
|
|
13
|
+
const row = where === 'now' ? marks.now : marks.rows;
|
|
14
|
+
const base = where === 'now' ? 0 : where * appData.files.length;
|
|
15
|
+
for (let i = 0; i < files.length; i++) {
|
|
16
|
+
if (row.charAt(base + files[i]) === '1') return appMetric[key];
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Класс клетки собирается в одном месте: и пометка приближения, и пропуск
|
|
22
|
+
* («файла нет») — свойства самой клетки, а не её содержимого. */
|
|
23
|
+
function appCellClass(first, miss, approx) {
|
|
24
|
+
return 'num' + (first ? ' g' : '') + (miss ? ' miss' : '') + (approx === null ? '' : ' approx');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Разметка клетки строки-коммита: правила — в cellParts, здесь только узел.
|
|
28
|
+
export function appCell(cell, first, approx) {
|
|
29
|
+
const parts = cellParts(cell.value, cell.delta, '−');
|
|
30
|
+
const td = appEl('td', appCellClass(first, parts.miss, approx));
|
|
31
|
+
if (approx !== null) td.title = appUi.approxCell + approx.method;
|
|
32
|
+
if (parts.dir === null) td.textContent = parts.text;
|
|
33
|
+
else td.appendChild(appEl('span', 'delta ' + parts.dir, parts.text));
|
|
34
|
+
return td;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Разметка клетки верхней строки: правила — в valueParts.
|
|
38
|
+
export function appValueCell(value, first, approx) {
|
|
39
|
+
const parts = valueParts(value);
|
|
40
|
+
const td = appEl('td', appCellClass(first, parts.miss, approx));
|
|
41
|
+
if (approx !== null) td.title = appUi.approxCell + approx.method;
|
|
42
|
+
td.textContent = parts.text;
|
|
43
|
+
return td;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Подпись коммита — той же разметкой, что в статической таблице: дата, тема,
|
|
47
|
+
* метка раздела журнала. Ширину колонки и обрезку длинной темы задаёт общая часть
|
|
48
|
+
* оформления, поэтому колонка не прыгает при переключении файлов. */
|
|
49
|
+
export function appCommit(row) {
|
|
50
|
+
const parts = commitParts(row, appData.report.showSha, row.href);
|
|
51
|
+
const name = parts.href ? appEl('a', 'subj', parts.subject) : appEl('span', 'subj', parts.subject);
|
|
52
|
+
if (parts.href) name.href = parts.href;
|
|
53
|
+
name.title = parts.title;
|
|
54
|
+
const mark = appEl('span', 'sect', parts.mark.text);
|
|
55
|
+
if (parts.mark.title) mark.title = parts.mark.title;
|
|
56
|
+
|
|
57
|
+
const clip = appEl('div', 'clip');
|
|
58
|
+
clip.appendChild(appEl('span', 'when', parts.when));
|
|
59
|
+
clip.appendChild(name);
|
|
60
|
+
clip.appendChild(mark);
|
|
61
|
+
const th = appEl('th', 'c-commit');
|
|
62
|
+
th.appendChild(clip);
|
|
63
|
+
return th;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function appSubHead(metrics) {
|
|
67
|
+
const tr = appEl('tr');
|
|
68
|
+
metrics.forEach((m, mi) => tr.appendChild(appEl('th', mi === 0 ? 'g' : '', m.label)));
|
|
69
|
+
return tr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Состояния пустоты: когда чисел не будет вовсе, страница говорит об этом словами,
|
|
73
|
+
* а не сеткой без колонок. Файлы можно выключить все — тогда остаётся общий объём,
|
|
74
|
+
* и подсказка объясняет, почему колонок нет. */
|
|
75
|
+
export function appState(metricsCount, filesCount) {
|
|
76
|
+
const state = document.getElementById('state');
|
|
77
|
+
const text = metricsCount === 0 ? appUi.empty : (filesCount === 0 ? appUi.noFiles : '');
|
|
78
|
+
state.textContent = text;
|
|
79
|
+
state.hidden = text === '';
|
|
80
|
+
document.getElementById('shell').hidden = metricsCount === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Шапка: строка групп (итог и файлы) и строка метрик под ней. Метрики повторяются
|
|
84
|
+
* на каждый файл, поэтому подшапка собирается один раз, а дальше её узлы
|
|
85
|
+
* переезжают в следующие — копий разметки не заводится. */
|
|
86
|
+
export function appHead(shown, files, metrics) {
|
|
87
|
+
const head = appEl('tr');
|
|
88
|
+
const commit = appEl('th', 'c-commit', appUi.commit);
|
|
89
|
+
commit.rowSpan = 2;
|
|
90
|
+
head.appendChild(commit);
|
|
91
|
+
const total = appEl('th', 'g', appUi.total);
|
|
92
|
+
total.colSpan = metrics.length;
|
|
93
|
+
head.appendChild(total);
|
|
94
|
+
files.forEach((i) => {
|
|
95
|
+
const th = appEl('th', 'g', appData.files[i].label);
|
|
96
|
+
th.colSpan = metrics.length;
|
|
97
|
+
head.appendChild(th);
|
|
98
|
+
});
|
|
99
|
+
const subs = appSubHead(shown);
|
|
100
|
+
files.forEach(() => {
|
|
101
|
+
const more = appSubHead(shown);
|
|
102
|
+
while (more.firstChild) subs.appendChild(more.firstChild);
|
|
103
|
+
});
|
|
104
|
+
const thead = appEl('thead');
|
|
105
|
+
thead.appendChild(head);
|
|
106
|
+
thead.appendChild(subs);
|
|
107
|
+
return thead;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Строка-коммит: подпись и числа. Дельты считает общий расчёт (`rowModel`) — тот
|
|
111
|
+
* же, что считает статическую таблицу; здесь только узлы. Пометка приближения —
|
|
112
|
+
* своя у каждой клетки: у итога по всем вошедшим файлам, у файла — по нему самому. */
|
|
113
|
+
export function appRow(r, metrics, files) {
|
|
114
|
+
const row = appData.rows[r];
|
|
115
|
+
const prev = r === 0 ? null : appData.rows[r - 1];
|
|
116
|
+
const model = rowModel(row.values, prev === null ? null : prev.values, metrics, appView.files);
|
|
117
|
+
const tr = appEl('tr');
|
|
118
|
+
tr.appendChild(appCommit(row));
|
|
119
|
+
model.total.forEach((cell, mi) => {
|
|
120
|
+
tr.appendChild(appCell(cell, mi === 0, appApprox(r, files, metrics[mi])));
|
|
121
|
+
});
|
|
122
|
+
model.files.forEach((cells, fi) => cells.forEach((cell, mi) => {
|
|
123
|
+
tr.appendChild(appCell(cell, mi === 0, appApprox(r, [files[fi]], metrics[mi])));
|
|
124
|
+
}));
|
|
125
|
+
return tr;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Тело: строки коммитов снизу вверх (свежие первыми) плюс верхняя строка «сейчас»
|
|
129
|
+
* с абсолютными размерами на HEAD. Дельты под ней сходятся с ней, поэтому она и
|
|
130
|
+
* стоит первой. */
|
|
131
|
+
export function appBody(metrics, files) {
|
|
132
|
+
const body = appEl('tbody');
|
|
133
|
+
for (let r = appData.rows.length - 1; r >= 0; r--) body.appendChild(appRow(r, metrics, files));
|
|
134
|
+
|
|
135
|
+
const now = appEl('tr', 'now');
|
|
136
|
+
now.appendChild(appEl('th', 'c-commit', appUi.now));
|
|
137
|
+
const nowCells = nowModel(appData.now, metrics, appView.files);
|
|
138
|
+
nowCells.total.forEach((v, mi) => now.appendChild(appValueCell(v, mi === 0,
|
|
139
|
+
appApprox('now', files, metrics[mi]))));
|
|
140
|
+
nowCells.files.forEach((cells, fi) => cells.forEach((v, mi) => {
|
|
141
|
+
now.appendChild(appValueCell(v, mi === 0, appApprox('now', [files[fi]], metrics[mi])));
|
|
142
|
+
}));
|
|
143
|
+
body.insertBefore(now, body.firstChild);
|
|
144
|
+
return body;
|
|
145
|
+
}
|