|
345 | 345 | color: var(--accent); |
346 | 346 | } |
347 | 347 |
|
| 348 | + /* Neutralized markdown image: no auto-fetch, just an inert label with URL */ |
| 349 | + .md-image { |
| 350 | + display: inline-block; |
| 351 | + color: var(--muted); |
| 352 | + font-style: italic; |
| 353 | + } |
| 354 | + .md-image-url { |
| 355 | + font-family: ui-monospace, "SF Mono", Menlo, monospace; |
| 356 | + font-size: 0.9em; |
| 357 | + opacity: 0.8; |
| 358 | + } |
| 359 | + |
348 | 360 | main.hide-thinking .e.thinking { |
349 | 361 | display: none; |
350 | 362 | } |
|
563 | 575 | ar: "ar-EG", |
564 | 576 | }; |
565 | 577 |
|
566 | | - const savedLang = localStorage.getItem("lang"); |
| 578 | + // localStorage can throw in strict privacy modes / third-party iframe contexts. |
| 579 | + const storage = { |
| 580 | + get(k) { |
| 581 | + try { |
| 582 | + return localStorage.getItem(k); |
| 583 | + } catch { |
| 584 | + return null; |
| 585 | + } |
| 586 | + }, |
| 587 | + set(k, v) { |
| 588 | + try { |
| 589 | + localStorage.setItem(k, v); |
| 590 | + } catch {} |
| 591 | + }, |
| 592 | + }; |
| 593 | + |
| 594 | + const savedLang = storage.get("lang"); |
567 | 595 | const browserLang = (navigator.language || "en").toLowerCase().split("-")[0]; |
568 | 596 | let locale = savedLang || (I18N[browserLang] ? browserLang : "en"); |
569 | 597 | const L = () => I18N[locale]; |
|
592 | 620 | const fbox = Object.fromEntries(FILTERS.map((k) => [k, document.getElementById("f-" + k)])); |
593 | 621 | Object.values(fbox).forEach((el) => el.addEventListener("change", applyFilters)); |
594 | 622 |
|
595 | | - marked.setOptions({ breaks: true, gfm: true }); |
| 623 | + // Privacy: neutralize markdown images (no auto-fetch) and restrict link schemes. |
| 624 | + // Without this, [text](...) and  in transcripts would fetch remote |
| 625 | + // resources on render and leak viewer usage to whoever crafted the transcript. |
| 626 | + const mdRenderer = new marked.Renderer(); |
| 627 | + mdRenderer.image = function (href, title, text) { |
| 628 | + const alt = esc(text || "image"); |
| 629 | + const url = href ? ` <span class="md-image-url">${esc(href)}</span>` : ""; |
| 630 | + return `<span class="md-image">🖼️ ${alt}${url}</span>`; |
| 631 | + }; |
| 632 | + mdRenderer.link = function (href, title, text) { |
| 633 | + // Only http(s) links are clickable; other schemes (javascript:, data:, file:, mailto:) |
| 634 | + // are rendered as plain text. `text` is already escaped/rendered by marked. |
| 635 | + if (!href || !/^https?:\/\//i.test(href)) return text; |
| 636 | + const t = title ? ` title="${esc(title)}"` : ""; |
| 637 | + return `<a href="${esc(href)}" rel="noopener noreferrer nofollow" target="_blank"${t}>${text}</a>`; |
| 638 | + }; |
| 639 | + marked.setOptions({ renderer: mdRenderer, breaks: true, gfm: true }); |
596 | 640 |
|
597 | 641 | // ===== truncation ===== |
598 | 642 | const MAX_PROSE = 20_000; |
|
620 | 664 |
|
621 | 665 | // ===== file loading ===== |
622 | 666 | async function loadFile(f) { |
| 667 | + // Clear stale stats from a previous file — on error the old stats row |
| 668 | + // would otherwise stay above the new error message and mislead the user. |
| 669 | + statsEl.textContent = ""; |
623 | 670 | chatEl.innerHTML = `<div class="empty">${L().reading}</div>`; |
624 | 671 | try { |
625 | 672 | const rows = await parseFile(f); |
|
990 | 1037 | langSel.value = locale; |
991 | 1038 | langSel.addEventListener("change", () => { |
992 | 1039 | locale = langSel.value; |
993 | | - localStorage.setItem("lang", locale); |
| 1040 | + storage.set("lang", locale); |
994 | 1041 | applyLocale(); |
995 | 1042 | renderFilterLabels(); |
996 | 1043 | if (state.rows.length) render(state.rows, state.fname); |
|
999 | 1046 |
|
1000 | 1047 | themeBtn.addEventListener("click", () => { |
1001 | 1048 | const next = document.documentElement.dataset.theme === "light" ? "dark" : "light"; |
1002 | | - localStorage.setItem("theme", next); |
| 1049 | + storage.set("theme", next); |
1003 | 1050 | applyTheme(next); |
1004 | 1051 | }); |
1005 | 1052 |
|
1006 | 1053 | applyLocale(); |
1007 | | - applyTheme(localStorage.getItem("theme") || "dark"); |
| 1054 | + applyTheme(storage.get("theme") || "dark"); |
1008 | 1055 | renderFilterLabels(); |
1009 | 1056 | renderEmptyState(); |
1010 | 1057 | </script> |
|
0 commit comments