max / makenotwork
9 files changed,
+23 insertions,
-33 deletions
| @@ -208,7 +208,4 @@ | |||
| 208 | 208 | return result; | |
| 209 | 209 | } | |
| 210 | 210 | ||
| 211 | - | function escapeHtml(s) { | |
| 212 | - | return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |
| 213 | - | } | |
| 214 | 211 | })(); |
| @@ -407,11 +407,6 @@ | |||
| 407 | 407 | ||
| 408 | 408 | // ── Shared ── | |
| 409 | 409 | ||
| 410 | - | function escapeHtml(s) { | |
| 411 | - | var d = document.createElement('div'); | |
| 412 | - | d.textContent = s; | |
| 413 | - | return d.innerHTML; | |
| 414 | - | } | |
| 415 | 410 | ||
| 416 | 411 | // Run on initial load | |
| 417 | 412 | init(); |
| @@ -202,7 +202,6 @@ | |||
| 202 | 202 | }); | |
| 203 | 203 | } | |
| 204 | 204 | ||
| 205 | - | function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } | |
| 206 | 205 | function escapeAttr(s) { return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); } | |
| 207 | 206 | ||
| 208 | 207 | // Upload all button |
| @@ -10,6 +10,18 @@ function csrfHeaders() { | |||
| 10 | 10 | return token ? { 'X-CSRF-Token': token } : {}; | |
| 11 | 11 | } | |
| 12 | 12 | ||
| 13 | + | /* Shared HTML escaper — the single definition (Run 20). Feature JS files use | |
| 14 | + | this global rather than each shipping their own; consolidating also removes | |
| 15 | + | the weaker per-file variants that escaped fewer characters. Escapes all five | |
| 16 | + | HTML-special characters, so it is safe for both element-content and attribute | |
| 17 | + | interpolation. mnw.js loads before every feature script, so the global is | |
| 18 | + | always available. */ | |
| 19 | + | function escapeHtml(s) { | |
| 20 | + | return String(s) | |
| 21 | + | .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| 22 | + | .replace(/"/g, '"').replace(/'/g, '''); | |
| 23 | + | } | |
| 24 | + | ||
| 13 | 25 | document.addEventListener('DOMContentLoaded', function() { | |
| 14 | 26 | // Read the csrf-token meta LIVE on every HTMX request rather than | |
| 15 | 27 | // snapshotting it at load. The token rotates mid-session (e.g. the 2FA | |
| @@ -530,7 +542,7 @@ function toggleShortcutsHelp() { | |||
| 530 | 542 | '<div class="modal-content" style="max-width: 420px; padding: 2rem;">' | |
| 531 | 543 | + '<div class="modal-header" style="margin-bottom: 1rem;">' | |
| 532 | 544 | + '<h2>Keyboard Shortcuts</h2>' | |
| 533 | - | + '<button type="button" class="modal-close" onclick="document.getElementById(\'shortcuts-help\').remove()">×</button>' | |
| 545 | + | + '<button type="button" class="modal-close" aria-label="Close">×</button>' | |
| 534 | 546 | + '</div>' | |
| 535 | 547 | + '<table style="width: 100%; font-size: 0.9rem;">' | |
| 536 | 548 | + '<tr><td style="padding: 0.3rem 0;"><kbd>Cmd+K</kbd></td><td>Search</td></tr>' | |
| @@ -540,6 +552,11 @@ function toggleShortcutsHelp() { | |||
| 540 | 552 | + '</table>' | |
| 541 | 553 | + '</div>'; | |
| 542 | 554 | ||
| 555 | + | // Wire the close button via JS, not an inline onclick — the app CSP has no | |
| 556 | + | // script-src 'unsafe-inline', so a generated inline handler is dead (Run 20). | |
| 557 | + | var closeBtn = overlay.querySelector('.modal-close'); | |
| 558 | + | if (closeBtn) closeBtn.onclick = function() { overlay.remove(); }; | |
| 559 | + | ||
| 543 | 560 | document.body.appendChild(overlay); | |
| 544 | 561 | } | |
| 545 | 562 |
| @@ -106,7 +106,6 @@ | |||
| 106 | 106 | items.forEach(function(el, i) { el.classList.toggle('highlighted', i === selectedIdx); }); | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 109 | - | function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; } | |
| 110 | 109 | ||
| 111 | 110 | document.addEventListener('click', function(e) { | |
| 112 | 111 | if (!box.contains(e.target) && e.target !== input) { box.style.display = 'none'; } |
| @@ -36,11 +36,6 @@ function renderMarkdownPreview() { | |||
| 36 | 36 | preview.innerHTML = html || '<p class="text-editor-placeholder">Nothing to preview...</p>'; | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | - | function escapeHtml(text) { | |
| 40 | - | const div = document.createElement('div'); | |
| 41 | - | div.textContent = text; | |
| 42 | - | return div.innerHTML; | |
| 43 | - | } | |
| 44 | 39 | ||
| 45 | 40 | function updateWordCount() { | |
| 46 | 41 | const body = document.getElementById('text-body').value; |
| @@ -7,11 +7,6 @@ | |||
| 7 | 7 | // reads the csrf-token meta live on each call, which matters because the | |
| 8 | 8 | // token rotates mid-session. Don't shadow it with a local copy. | |
| 9 | 9 | ||
| 10 | - | function escapeHtml(s) { | |
| 11 | - | var d = document.createElement('div'); | |
| 12 | - | d.textContent = s; | |
| 13 | - | return d.innerHTML; | |
| 14 | - | } | |
| 15 | 10 | ||
| 16 | 11 | function showToast(msg) { | |
| 17 | 12 | if (window.showToast) { window.showToast(msg); return; } |
| @@ -40,10 +40,13 @@ function showPromoRedemptions(codeId, codeLabel) { | |||
| 40 | 40 | content.innerHTML = | |
| 41 | 41 | '<div class="modal-header" style="margin-bottom: 1rem;">' | |
| 42 | 42 | + '<h2>Redemptions: <code>' + escapeHtml(codeLabel) + '</code></h2>' | |
| 43 | - | + '<button type="button" class="modal-close" aria-label="Dismiss"' | |
| 44 | - | + ' onclick="document.getElementById(\'promo-redemptions-modal\').remove()">×</button>' | |
| 43 | + | + '<button type="button" class="modal-close" aria-label="Dismiss">×</button>' | |
| 45 | 44 | + '</div>' | |
| 46 | 45 | + '<div id="promo-redemptions-body" class="muted">Loading…</div>'; | |
| 46 | + | // Wire the close button via JS, not an inline onclick — the app CSP has no | |
| 47 | + | // script-src 'unsafe-inline', so a generated inline handler is dead (Run 20). | |
| 48 | + | var closeBtn = content.querySelector('.modal-close'); | |
| 49 | + | if (closeBtn) closeBtn.onclick = function () { overlay.remove(); }; | |
| 47 | 50 | overlay.appendChild(content); | |
| 48 | 51 | document.body.appendChild(overlay); | |
| 49 | 52 | ||
| @@ -81,11 +84,6 @@ function showPromoRedemptions(codeId, codeLabel) { | |||
| 81 | 84 | }); | |
| 82 | 85 | } | |
| 83 | 86 | ||
| 84 | - | function escapeHtml(s) { | |
| 85 | - | return String(s) | |
| 86 | - | .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') | |
| 87 | - | .replace(/"/g, '"').replace(/'/g, '''); | |
| 88 | - | } | |
| 89 | 87 | ||
| 90 | 88 | /// Switch between preset trial lengths and a custom number input. The | |
| 91 | 89 | /// hidden #pc-trial-days carries the actual submitted value so we can keep |
| @@ -42,11 +42,6 @@ | |||
| 42 | 42 | queueEl.classList.remove('hidden'); | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | - | function escapeHtml(s) { | |
| 46 | - | var d = document.createElement('div'); | |
| 47 | - | d.textContent = s; | |
| 48 | - | return d.innerHTML; | |
| 49 | - | } | |
| 50 | 45 | ||
| 51 | 46 | function escapeAttr(s) { | |
| 52 | 47 | return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); |