@magland/mochi 0.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/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pageScript = pageScript;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
// The one script every page loads.
|
|
6
|
+
//
|
|
7
|
+
// This used to be two inline <script> blocks in layout(), which worked and
|
|
8
|
+
// cost nothing extra to fetch, but meant the pages could never carry a
|
|
9
|
+
// Content-Security-Policy worth having: a policy that allows inline script
|
|
10
|
+
// allows an injected one too, and the browser cannot tell them apart. So the
|
|
11
|
+
// script lives here, is served from /assets/page.js, and the pages carry no
|
|
12
|
+
// executable markup of their own.
|
|
13
|
+
//
|
|
14
|
+
// It is the same bytes for every vault, every theme, and every page, which is
|
|
15
|
+
// what lets it be cached for good: the tag below is a hash of the body, so an
|
|
16
|
+
// edit to this file changes the URL. Anything page-specific reaches it through
|
|
17
|
+
// a data attribute rather than through interpolation -- the vault's theme on
|
|
18
|
+
// <html>, a live job's log endpoint on the <pre> that shows it -- which is
|
|
19
|
+
// also why there is nothing here for a template to escape.
|
|
20
|
+
//
|
|
21
|
+
// Style, for the same reason the rest of the frontend has no build step: this
|
|
22
|
+
// is ES5-shaped, no modules, no bundler, and it is loaded in <head> without
|
|
23
|
+
// defer so that the theme is applied before the first paint.
|
|
24
|
+
const PAGE_JS = `
|
|
25
|
+
// ---- appearance ----
|
|
26
|
+
|
|
27
|
+
// The appearance the reader chose, applied before the page is painted so the
|
|
28
|
+
// vault's own theme is never shown for a frame first. The stylesheet carries
|
|
29
|
+
// every theme's tokens, so this is one attribute; the code colours are a whole
|
|
30
|
+
// stylesheet of their own and so are a second request when they differ.
|
|
31
|
+
// The vault's own theme and the dark one it pairs with are stamped on <html>
|
|
32
|
+
// by the server, so this file is the same bytes for every vault and every
|
|
33
|
+
// theme and can be cached for good.
|
|
34
|
+
var mochiRoot = document.documentElement;
|
|
35
|
+
var mochiTheme = {
|
|
36
|
+
vault: mochiRoot.getAttribute('data-theme-vault') || 'paper',
|
|
37
|
+
dark: mochiRoot.getAttribute('data-theme-dark') || 'midnight',
|
|
38
|
+
};
|
|
39
|
+
function applyTheme() {
|
|
40
|
+
var pick = null;
|
|
41
|
+
try { pick = localStorage.getItem('mochi.theme'); } catch (e) {}
|
|
42
|
+
var auto = !pick || pick === 'auto';
|
|
43
|
+
if (auto) pick = matchMedia('(prefers-color-scheme: dark)').matches ? mochiTheme.dark : mochiTheme.vault;
|
|
44
|
+
document.documentElement.setAttribute('data-theme', pick);
|
|
45
|
+
var hl = document.getElementById('hl-css');
|
|
46
|
+
var href = '/assets/hl.css?t=' + encodeURIComponent(pick);
|
|
47
|
+
if (hl && hl.getAttribute('href') !== href) hl.setAttribute('href', href);
|
|
48
|
+
var items = document.querySelectorAll('[data-theme-name]');
|
|
49
|
+
for (var i = 0; i < items.length; i++) {
|
|
50
|
+
var n = items[i].getAttribute('data-theme-name');
|
|
51
|
+
items[i].setAttribute('aria-checked', String(auto ? n === 'auto' : n === pick));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function setTheme(name) {
|
|
55
|
+
try { localStorage.setItem('mochi.theme', name); } catch (e) {}
|
|
56
|
+
applyTheme();
|
|
57
|
+
if (typeof closeMenus === 'function') closeMenus(null);
|
|
58
|
+
}
|
|
59
|
+
applyTheme();
|
|
60
|
+
// Again once the menu exists: the call above runs before the body is parsed,
|
|
61
|
+
// which is the point of it, but it means the menu's marks are set here.
|
|
62
|
+
document.addEventListener('DOMContentLoaded', applyTheme);
|
|
63
|
+
// Following the system means following it while the page is open, not only
|
|
64
|
+
// when it was loaded.
|
|
65
|
+
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
|
|
66
|
+
|
|
67
|
+
// ---- copying, menus, filtering, and the jump box ----
|
|
68
|
+
|
|
69
|
+
function copyText(btn, text) {
|
|
70
|
+
function done() { btn.classList.add('copied'); setTimeout(function () { btn.classList.remove('copied'); }, 1400); }
|
|
71
|
+
function fallback() {
|
|
72
|
+
var ta = document.createElement('textarea');
|
|
73
|
+
ta.value = text;
|
|
74
|
+
document.body.appendChild(ta);
|
|
75
|
+
ta.select();
|
|
76
|
+
try { document.execCommand('copy'); } catch (e) {}
|
|
77
|
+
ta.remove();
|
|
78
|
+
}
|
|
79
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
80
|
+
navigator.clipboard.writeText(text).then(done, function () { fallback(); done(); });
|
|
81
|
+
} else { fallback(); done(); }
|
|
82
|
+
}
|
|
83
|
+
// The text to copy is the button's own data-copy when it carries one, and
|
|
84
|
+
// otherwise whatever sits just before it: a <code>, or an <input> holding a URL.
|
|
85
|
+
function copyCmd(btn) {
|
|
86
|
+
var el = btn.previousElementSibling;
|
|
87
|
+
var own = btn.getAttribute('data-copy');
|
|
88
|
+
copyText(btn, own !== null ? own : (el && el.tagName === 'INPUT' ? el.value : el.textContent));
|
|
89
|
+
}
|
|
90
|
+
// A file view is one element per line, so its text is gathered rather than
|
|
91
|
+
// read off one node; the line numbers are separate elements and stay out.
|
|
92
|
+
function copyLines(btn) {
|
|
93
|
+
var lines = document.querySelectorAll('.code-lines .ltext');
|
|
94
|
+
var out = [];
|
|
95
|
+
for (var i = 0; i < lines.length; i++) out.push(lines[i].textContent);
|
|
96
|
+
copyText(btn, out.join('\\n'));
|
|
97
|
+
}
|
|
98
|
+
// Menus are <details> elements. These two handlers give them the rest of what
|
|
99
|
+
// a menu is expected to do: close when the reader clicks elsewhere or presses
|
|
100
|
+
// Escape. Nothing else about them needs script.
|
|
101
|
+
function closeMenus(except) {
|
|
102
|
+
var open = document.querySelectorAll('details.dropdown[open]');
|
|
103
|
+
for (var i = 0; i < open.length; i++) {
|
|
104
|
+
if (!except || !open[i].contains(except)) open[i].open = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
document.addEventListener('click', function (e) { closeMenus(e.target); });
|
|
108
|
+
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeMenus(null); });
|
|
109
|
+
// The file finder's matching: every character of the query in order, though
|
|
110
|
+
// not necessarily together, so "srcmn" finds src/compute/mean.py. Matching is
|
|
111
|
+
// done here rather than on the server because the whole list is already in
|
|
112
|
+
// the page.
|
|
113
|
+
function findMatch(hay, needle) {
|
|
114
|
+
var i = 0;
|
|
115
|
+
for (var j = 0; j < hay.length && i < needle.length; j++) {
|
|
116
|
+
if (hay.charCodeAt(j) === needle.charCodeAt(i)) i++;
|
|
117
|
+
}
|
|
118
|
+
return i === needle.length;
|
|
119
|
+
}
|
|
120
|
+
function filterFiles(input) {
|
|
121
|
+
var items = document.getElementById('find-list').children;
|
|
122
|
+
var q = input.value.trim().toLowerCase();
|
|
123
|
+
var shown = 0;
|
|
124
|
+
for (var i = 0; i < items.length; i++) {
|
|
125
|
+
var hit = q === '' || findMatch(items[i].textContent.toLowerCase(), q);
|
|
126
|
+
items[i].hidden = !hit;
|
|
127
|
+
if (hit) shown++;
|
|
128
|
+
}
|
|
129
|
+
document.getElementById('find-empty').hidden = shown !== 0;
|
|
130
|
+
}
|
|
131
|
+
// Enter opens the first match, Escape clears the box.
|
|
132
|
+
function findKey(e, input) {
|
|
133
|
+
if (e.key === 'Escape') { input.value = ''; filterFiles(input); return; }
|
|
134
|
+
if (e.key !== 'Enter') return;
|
|
135
|
+
var items = document.getElementById('find-list').children;
|
|
136
|
+
for (var i = 0; i < items.length; i++) {
|
|
137
|
+
if (!items[i].hidden) { e.preventDefault(); location.href = items[i].href; return; }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ---- the jump box ----
|
|
141
|
+
// The repository names are the same list the front page shows, fetched once
|
|
142
|
+
// and kept for the tab, so typing costs nothing after the first opening.
|
|
143
|
+
var jumpRepos = null;
|
|
144
|
+
var jumpCtx = null;
|
|
145
|
+
var jumpSel = 0;
|
|
146
|
+
(function () {
|
|
147
|
+
var el = document.getElementById('jump-data');
|
|
148
|
+
if (el) { try { jumpCtx = JSON.parse(el.textContent); } catch (e) {} }
|
|
149
|
+
})();
|
|
150
|
+
function loadJumpRepos() {
|
|
151
|
+
if (jumpRepos) return Promise.resolve(jumpRepos);
|
|
152
|
+
var cached = null;
|
|
153
|
+
try { cached = sessionStorage.getItem('mochi.repos'); } catch (e) {}
|
|
154
|
+
if (cached) { try { jumpRepos = JSON.parse(cached); return Promise.resolve(jumpRepos); } catch (e) {} }
|
|
155
|
+
return fetch('/assets/repos.json').then(function (r) { return r.json(); }).then(function (list) {
|
|
156
|
+
jumpRepos = list;
|
|
157
|
+
try { sessionStorage.setItem('mochi.repos', JSON.stringify(list)); } catch (e) {}
|
|
158
|
+
return list;
|
|
159
|
+
}, function () { jumpRepos = []; return jumpRepos; });
|
|
160
|
+
}
|
|
161
|
+
function openJump() {
|
|
162
|
+
var dlg = document.getElementById('jump');
|
|
163
|
+
if (!dlg || dlg.open) return;
|
|
164
|
+
closeMenus(null);
|
|
165
|
+
dlg.showModal();
|
|
166
|
+
var q = document.getElementById('jump-q');
|
|
167
|
+
q.value = '';
|
|
168
|
+
q.focus();
|
|
169
|
+
renderJump();
|
|
170
|
+
loadJumpRepos().then(renderJump);
|
|
171
|
+
}
|
|
172
|
+
// The score is the position of the first character of the match, so a name
|
|
173
|
+
// beginning with what was typed comes before one that merely contains it, and
|
|
174
|
+
// a match on the repository's own name before one on its collection.
|
|
175
|
+
function jumpScore(hay, q) {
|
|
176
|
+
if (q === '') return 0;
|
|
177
|
+
var i = hay.toLowerCase().indexOf(q);
|
|
178
|
+
if (i !== -1) return i;
|
|
179
|
+
return findMatch(hay.toLowerCase(), q) ? 900 : -1;
|
|
180
|
+
}
|
|
181
|
+
function jumpItems() {
|
|
182
|
+
var q = document.getElementById('jump-q').value.trim().toLowerCase();
|
|
183
|
+
var out = [];
|
|
184
|
+
if (jumpCtx) {
|
|
185
|
+
for (var s = 0; s < jumpCtx.sections.length; s++) {
|
|
186
|
+
var sec = jumpCtx.sections[s];
|
|
187
|
+
var ss = jumpScore(sec.label, q);
|
|
188
|
+
if (ss >= 0) out.push({ score: ss, group: jumpCtx.repo, label: sec.label, href: sec.href });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
var repos = jumpRepos || [];
|
|
192
|
+
for (var i = 0; i < repos.length; i++) {
|
|
193
|
+
var name = repos[i];
|
|
194
|
+
var slash = name.indexOf('/');
|
|
195
|
+
var short = name.slice(slash + 1);
|
|
196
|
+
var sc = jumpScore(short, q);
|
|
197
|
+
// Falling back to the whole path lets "concept/bench" find a repository,
|
|
198
|
+
// but only as a literal substring: a collection name spread across it a
|
|
199
|
+
// letter at a time would match nearly everything.
|
|
200
|
+
if (sc < 0 && q !== '' && name.toLowerCase().indexOf(q) !== -1) sc = 950;
|
|
201
|
+
// A repository is what the box is mostly for, so its matches sort above a
|
|
202
|
+
// section of equal quality rather than below.
|
|
203
|
+
if (sc >= 0) out.push({ score: sc - 1, group: 'Repositories', label: short, note: name.slice(0, slash), href: '/' + name.split('/').map(encodeURIComponent).join('/') });
|
|
204
|
+
}
|
|
205
|
+
out.sort(function (a, b) { return a.score - b.score || a.label.localeCompare(b.label); });
|
|
206
|
+
out = out.slice(0, 15);
|
|
207
|
+
if (jumpCtx && q !== '') {
|
|
208
|
+
var g = 'Search ' + jumpCtx.repo;
|
|
209
|
+
out.push({ group: g, label: 'Contents matching \u201c' + q + '\u201d', href: jumpCtx.searchUrl + encodeURIComponent(q) });
|
|
210
|
+
out.push({ group: g, label: 'File names', href: jumpCtx.findUrl });
|
|
211
|
+
}
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
function renderJump() {
|
|
215
|
+
var list = document.getElementById('jump-list');
|
|
216
|
+
if (!list) return;
|
|
217
|
+
var items = jumpItems();
|
|
218
|
+
if (jumpSel >= items.length) jumpSel = 0;
|
|
219
|
+
var html = '';
|
|
220
|
+
var group = null;
|
|
221
|
+
for (var i = 0; i < items.length; i++) {
|
|
222
|
+
var it = items[i];
|
|
223
|
+
if (it.group !== group) { group = it.group; html += '<li class="jump-group" role="presentation">' + escapeHtml(group) + '</li>'; }
|
|
224
|
+
html += '<li role="option" aria-selected="' + (i === jumpSel) + '"><a class="jump-item' + (i === jumpSel ? ' on' : '') +
|
|
225
|
+
'" href="' + escapeHtml(it.href) + '" data-jump-index="' + i + '">' + escapeHtml(it.label) +
|
|
226
|
+
(it.note ? '<span class="jump-note">' + escapeHtml(it.note) + '</span>' : '') + '</a></li>';
|
|
227
|
+
}
|
|
228
|
+
list.innerHTML = html || '<li class="jump-empty" role="presentation">' +
|
|
229
|
+
(jumpRepos === null ? 'Loading\u2026' : 'Nothing matches.') + '</li>';
|
|
230
|
+
var on = list.querySelector('.jump-item.on');
|
|
231
|
+
if (on) on.scrollIntoView({ block: 'nearest' });
|
|
232
|
+
}
|
|
233
|
+
function escapeHtml(s) {
|
|
234
|
+
return String(s).replace(/[&<>"']/g, function (c) {
|
|
235
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
function jumpPoint(i) { jumpSel = i; renderJump(); }
|
|
239
|
+
function jumpKey(e) {
|
|
240
|
+
var items = document.getElementById('jump-list').querySelectorAll('.jump-item');
|
|
241
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
if (!items.length) return;
|
|
244
|
+
jumpSel = (jumpSel + (e.key === 'ArrowDown' ? 1 : items.length - 1)) % items.length;
|
|
245
|
+
renderJump();
|
|
246
|
+
} else if (e.key === 'Enter') {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
if (items[jumpSel]) location.href = items[jumpSel].getAttribute('href');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// / opens the box and t still goes straight to the file finder. A key pressed
|
|
252
|
+
// while typing into something is a keystroke, not a shortcut.
|
|
253
|
+
document.addEventListener('keydown', function (e) {
|
|
254
|
+
var el = document.activeElement;
|
|
255
|
+
if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.isContentEditable)) return;
|
|
256
|
+
if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) { e.preventDefault(); openJump(); return; }
|
|
257
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
258
|
+
if (e.key === '/') { e.preventDefault(); openJump(); return; }
|
|
259
|
+
if (e.key !== 't') return;
|
|
260
|
+
var btn = document.querySelector('[data-find-url]');
|
|
261
|
+
if (!btn) return;
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
location.href = btn.getAttribute('data-find-url');
|
|
264
|
+
});
|
|
265
|
+
// Clicking the backdrop closes the box, as clicking outside any other menu does.
|
|
266
|
+
document.addEventListener('click', function (e) {
|
|
267
|
+
var dlg = document.getElementById('jump');
|
|
268
|
+
if (dlg && dlg.open && e.target === dlg) dlg.close();
|
|
269
|
+
});
|
|
270
|
+
// The filter box above a listing: hide the rows that do not match, and say so
|
|
271
|
+
// when none do.
|
|
272
|
+
function filterRows(input) {
|
|
273
|
+
var id = input.getAttribute('data-target');
|
|
274
|
+
var table = document.getElementById(id);
|
|
275
|
+
var empty = document.getElementById(id + '-empty');
|
|
276
|
+
var q = input.value.trim().toLowerCase();
|
|
277
|
+
var rows = table.tBodies[0].rows;
|
|
278
|
+
var shown = 0;
|
|
279
|
+
for (var i = 0; i < rows.length; i++) {
|
|
280
|
+
var hit = q === '' || rows[i].textContent.toLowerCase().indexOf(q) !== -1;
|
|
281
|
+
rows[i].hidden = !hit;
|
|
282
|
+
if (hit) shown++;
|
|
283
|
+
}
|
|
284
|
+
if (empty) empty.hidden = shown !== 0;
|
|
285
|
+
}
|
|
286
|
+
// The same as filterRows, over a list of cards rather than table rows.
|
|
287
|
+
function filterCards(input) {
|
|
288
|
+
var list = document.getElementById(input.getAttribute('data-target'));
|
|
289
|
+
var empty = document.getElementById(input.getAttribute('data-target') + '-empty');
|
|
290
|
+
var q = input.value.trim().toLowerCase();
|
|
291
|
+
var items = list.children;
|
|
292
|
+
var shown = 0;
|
|
293
|
+
for (var i = 0; i < items.length; i++) {
|
|
294
|
+
var hit = q === '' || items[i].textContent.toLowerCase().indexOf(q) !== -1;
|
|
295
|
+
items[i].hidden = !hit;
|
|
296
|
+
if (hit) shown++;
|
|
297
|
+
}
|
|
298
|
+
if (empty) empty.hidden = shown !== 0;
|
|
299
|
+
}
|
|
300
|
+
// The filter box in a menu of many items (branches and tags).
|
|
301
|
+
function filterMenu(input) {
|
|
302
|
+
var menu = input.parentElement;
|
|
303
|
+
var q = input.value.trim().toLowerCase();
|
|
304
|
+
var groups = menu.querySelectorAll('.dd-group');
|
|
305
|
+
for (var g = 0; g < groups.length; g++) {
|
|
306
|
+
var items = groups[g].querySelectorAll('.dd-item');
|
|
307
|
+
var shown = 0;
|
|
308
|
+
for (var i = 0; i < items.length; i++) {
|
|
309
|
+
var hit = q === '' || items[i].textContent.toLowerCase().indexOf(q) !== -1;
|
|
310
|
+
items[i].hidden = !hit;
|
|
311
|
+
if (hit) shown++;
|
|
312
|
+
}
|
|
313
|
+
groups[g].hidden = shown === 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---- wiring ----
|
|
318
|
+
//
|
|
319
|
+
// The markup carries data attributes rather than inline handlers, and the
|
|
320
|
+
// listeners are attached here. That is what a Content-Security-Policy without
|
|
321
|
+
// 'unsafe-inline' requires, and it costs the pages nothing: an attribute like
|
|
322
|
+
// data-confirm is data, so the template escapes it like any other value, where
|
|
323
|
+
// onsubmit="return confirm('...')" put a name inside a JavaScript string
|
|
324
|
+
// inside an HTML attribute and depended on the name never containing a quote.
|
|
325
|
+
//
|
|
326
|
+
// Delegated from the document wherever the elements come and go (a menu
|
|
327
|
+
// re-rendered, a dialog filled in), so nothing has to be re-attached.
|
|
328
|
+
|
|
329
|
+
// The nearest ancestor matching a selector, including the element itself.
|
|
330
|
+
function closestOf(el, selector) {
|
|
331
|
+
while (el && el.nodeType === 1) {
|
|
332
|
+
if (el.matches && el.matches(selector)) return el;
|
|
333
|
+
el = el.parentElement;
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
document.addEventListener('click', function (e) {
|
|
339
|
+
var t = e.target;
|
|
340
|
+
if (closestOf(t, '[data-jump-open]')) { openJump(); return; }
|
|
341
|
+
var theme = closestOf(t, '[data-theme-name]');
|
|
342
|
+
if (theme) { setTheme(theme.getAttribute('data-theme-name')); return; }
|
|
343
|
+
var copy = closestOf(t, '.copy-btn');
|
|
344
|
+
if (copy) { copyCmd(copy); return; }
|
|
345
|
+
var lines = closestOf(t, '[data-copy-lines]');
|
|
346
|
+
if (lines) { copyLines(lines); return; }
|
|
347
|
+
var select = closestOf(t, '[data-select-all]');
|
|
348
|
+
if (select) { select.select(); return; }
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
document.addEventListener('input', function (e) {
|
|
352
|
+
var el = e.target;
|
|
353
|
+
if (!el || !el.matches) return;
|
|
354
|
+
if (el.matches('.dd-filter')) { filterMenu(el); return; }
|
|
355
|
+
if (el.matches('.find-input')) { filterFiles(el); return; }
|
|
356
|
+
if (el.id === 'jump-q') { renderJump(); return; }
|
|
357
|
+
if (el.matches('[data-filter="rows"]')) { filterRows(el); return; }
|
|
358
|
+
if (el.matches('[data-filter="cards"]')) { filterCards(el); return; }
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
document.addEventListener('change', function (e) {
|
|
362
|
+
var el = e.target;
|
|
363
|
+
if (el && el.matches && el.matches('[data-workflow-picker]')) pickWorkflow(el);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
document.addEventListener('keydown', function (e) {
|
|
367
|
+
var el = e.target;
|
|
368
|
+
if (!el || !el.matches) return;
|
|
369
|
+
if (el.id === 'jump-q') { jumpKey(e); return; }
|
|
370
|
+
if (el.matches('.find-input')) { findKey(e, el); return; }
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// mouseenter does not bubble, so the jump list is followed with mouseover,
|
|
374
|
+
// which does; the guard keeps a move within one row from re-rendering.
|
|
375
|
+
document.addEventListener('mouseover', function (e) {
|
|
376
|
+
var item = closestOf(e.target, '[data-jump-index]');
|
|
377
|
+
if (!item) return;
|
|
378
|
+
var i = parseInt(item.getAttribute('data-jump-index'), 10);
|
|
379
|
+
if (i !== jumpSel) jumpPoint(i);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// A form that carries data-confirm asks before it submits. Anything it says is
|
|
383
|
+
// the server's text, put in an attribute rather than in script.
|
|
384
|
+
document.addEventListener('submit', function (e) {
|
|
385
|
+
var form = closestOf(e.target, 'form[data-confirm]');
|
|
386
|
+
if (!form) return;
|
|
387
|
+
if (!confirm(form.getAttribute('data-confirm'))) e.preventDefault();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// The workflow dispatch menu shows one form per workflow and the picker
|
|
391
|
+
// chooses between them. It lived beside the menu it belongs to; it is here now
|
|
392
|
+
// because that is where every listener is.
|
|
393
|
+
function pickWorkflow(sel) {
|
|
394
|
+
var root = closestOf(sel, '.dispatch-body');
|
|
395
|
+
if (!root) return;
|
|
396
|
+
var forms = root.querySelectorAll('.dispatch-panel');
|
|
397
|
+
for (var i = 0; i < forms.length; i++) {
|
|
398
|
+
forms[i].hidden = forms[i].getAttribute('data-wf') !== sel.value;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// The editor: Tab indents rather than leaving the field, since code is what is
|
|
403
|
+
// being typed (Shift-Tab still moves focus out, so the keyboard is not
|
|
404
|
+
// trapped), and navigating away from an edited buffer asks first. Committing
|
|
405
|
+
// does not ask; submitting is the point.
|
|
406
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
407
|
+
var ta = document.querySelector('textarea.code-editor');
|
|
408
|
+
if (!ta) return;
|
|
409
|
+
var form = ta.closest('form');
|
|
410
|
+
var initial = ta.value;
|
|
411
|
+
var submitted = false;
|
|
412
|
+
if (form) form.addEventListener('submit', function () { submitted = true; });
|
|
413
|
+
window.addEventListener('beforeunload', function (e) {
|
|
414
|
+
if (submitted || ta.value === initial) return;
|
|
415
|
+
e.preventDefault();
|
|
416
|
+
e.returnValue = '';
|
|
417
|
+
});
|
|
418
|
+
ta.addEventListener('keydown', function (e) {
|
|
419
|
+
if (e.key !== 'Tab' || e.shiftKey || e.ctrlKey || e.altKey || e.metaKey) return;
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
ta.setRangeText(' ', ta.selectionStart, ta.selectionEnd, 'end');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// A live job's log: the endpoint and the offset to resume from are on the
|
|
426
|
+
// element, so this file stays the same bytes whatever job is being watched.
|
|
427
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
428
|
+
var el = document.getElementById('livelog');
|
|
429
|
+
if (!el || !el.getAttribute('data-log-url')) return;
|
|
430
|
+
var url = el.getAttribute('data-log-url');
|
|
431
|
+
var offset = parseInt(el.getAttribute('data-log-offset') || '0', 10);
|
|
432
|
+
var stick = true;
|
|
433
|
+
el.addEventListener('scroll', function () {
|
|
434
|
+
stick = el.scrollTop + el.clientHeight >= el.scrollHeight - 8;
|
|
435
|
+
});
|
|
436
|
+
function poll() {
|
|
437
|
+
fetch(url + '?offset=' + offset, { headers: { accept: 'application/json' } })
|
|
438
|
+
.then(function (r) { return r.json(); })
|
|
439
|
+
.then(function (d) {
|
|
440
|
+
if (d.lines && d.lines.length) {
|
|
441
|
+
el.textContent += (el.textContent ? '\\n' : '') + d.lines.join('\\n');
|
|
442
|
+
if (stick) el.scrollTop = el.scrollHeight;
|
|
443
|
+
}
|
|
444
|
+
offset = d.offset;
|
|
445
|
+
if (d.done) { location.reload(); return; }
|
|
446
|
+
setTimeout(poll, 1500);
|
|
447
|
+
})
|
|
448
|
+
.catch(function () { setTimeout(poll, 5000); });
|
|
449
|
+
}
|
|
450
|
+
el.scrollTop = el.scrollHeight;
|
|
451
|
+
setTimeout(poll, 1000);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
`;
|
|
455
|
+
/**
|
|
456
|
+
* The script and a tag naming this exact body, in the shape styleSheet() uses
|
|
457
|
+
* and for the same reason: a URL carrying the right tag may be kept for good.
|
|
458
|
+
*/
|
|
459
|
+
let made = null;
|
|
460
|
+
function pageScript() {
|
|
461
|
+
if (!made) {
|
|
462
|
+
made = { body: PAGE_JS, tag: (0, crypto_1.createHash)('sha256').update(PAGE_JS).digest('hex').slice(0, 12) };
|
|
463
|
+
}
|
|
464
|
+
return made;
|
|
465
|
+
}
|