@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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. 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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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
+ }