Files
anonymous_github/public/script/main.js
T
tdurieuxandClaude Opus 5.5 57412d0610 feat: refine the reviewer explorer and the visual system
- Explorer: redacted terms are highlighted in Markdown and code, the
  sidebar shows when the link expires, "Download repository" is the one
  primary action, and signed-out reviewers get a header without Sign in
  or the source-code link.
- Status: a ready anonymization leads with a copyable share link; the
  support links sit flat inside their section.
- One primary colour (accent) for actions and progress; mono is kept for
  IDs, hashes and paths; settings are plain sections instead of cards.
- Font sizes snap to one scale (10-40px); rem values keep following the
  reader's browser font size.
- Ko-fi button uses a dark surface in dark mode instead of a white pill.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:45:54 +02:00

150 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { loadLibrary } from "./lazy-assets.js";
import { createApp, h, watch, provide, inject, onBeforeUnmount } from "vue";
import { createRouter, createWebHistory, RouterView } from "vue-router";
import { pageRoutes } from "./routes.js";
import { templates } from "./templates.js";
import { render as iconsTemplate } from "../partials/icons.htm";
import { initializeTemplate } from "./template-state.js";
import { createPageState, createTimers } from "./state.js";
import { createHttp, promises } from "./http.js";
import { mainController, highlightRedactions } from "./app.js";
import { createQuotaService } from "./quota.js";
import * as formatters from "./formatters.js";
import translations from "../i18n/locale-en.json";
import { components, codeEditor, paperScrollspy } from "./components.js";
import Tree from "./tree.js";
import { fieldDirective, formDirective, submitForm } from "./forms.js";
const sessionKey = Symbol("session");
export const serverPaths = /^\/(w|api|github)(\/|$)/;
function translate(key, params = {}) {
let message = key?.split(".").reduce((value, part) => value?.[part], translations);
// Unknown error codes would otherwise surface as the raw "ERRORS.code" key.
if (message == null && key?.startsWith("ERRORS.")) message = translations.ERRORS.unknown_error;
return String(message ?? key ?? "").replace(/{{\s*([^}]+?)\s*}}/g, (_, name) => params[name] ?? "");
}
const fmt = { ...formatters, translate };
export function safeUrl(value) {
if (value == null || value === "") return value;
try {
const url = new URL(value, window.location.href);
return ["http:", "https:", "mailto:", "tel:", "blob:"].includes(url.protocol) ? value : undefined;
} catch { return undefined; }
}
export function mountApplication(target = "#app", options = {}) {
// AngularJS links stored the repository route in the fragment. Normalize it
// before web history reads the URL, replacing the entry so Back still works.
if (!options.history && window.location.pathname === "/" && /^#!\/r\/[^/?#]+/.test(window.location.hash)) {
window.history.replaceState(window.history.state, "", window.location.hash.slice(2));
}
const events = new Map();
const http = createHttp(options.fetch);
let root;
let router;
function services() {
const timers = createTimers();
const location = {
url(value) { if (value === undefined) return router.currentRoute.value.fullPath; router.push(value); return location; },
path() { return router.currentRoute.value.path; },
search() { return router.currentRoute.value.query; },
};
const params = new Proxy({}, { get(_, key) {
const value = router.currentRoute.value.params[key];
return Array.isArray(value) ? value.join("/") : value;
} });
return { http, ...timers, location, params, window, promises, html: { trustAsHtml: value => value }, translate: (key, params) => Promise.resolve(translate(key, params)), quotaService: createQuotaService(http) };
}
function pageComponent(definition) {
return {
name: definition.template,
setup() {
const state = createPageState(inject(sessionKey), events);
definition.setup(state, services());
initializeTemplate(definition.template, state);
const cache = [];
return () => templates[definition.template](state, cache);
},
};
}
router = createRouter({
history: options.history || createWebHistory(),
routes: pageRoutes.map(definition => definition.redirect ? definition : ({
path: definition.path,
component: pageComponent(definition),
meta: { title: definition.title, preserveExplorer: definition.preserveExplorer },
})),
});
const app = createApp({
setup() {
root = createPageState(null, events);
root.fmt = fmt;
root.window = window;
root.Math = Math;
root.sanitize = value => DOMPurify.sanitize(value ?? "");
root.highlightRedactions = html => highlightRedactions(html, root.site_options?.ANONYMIZATION_MASK);
root.submitForm = submitForm;
root.safeUrl = safeUrl;
const context = services();
mainController(root, context.http, context.location, context.timeout);
provide(sessionKey, root);
watch(() => root.title, title => { document.title = title || "Anonymous GitHub"; });
const headerCache = [];
const navigate = event => {
const anchor = event.target.closest?.("a[href]");
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !anchor || anchor.hasAttribute("download") || (anchor.target && anchor.target !== "_self") || anchor.dataset.toggle) return;
const href = anchor.getAttribute("href");
if (href.startsWith("#")) return;
const url = new URL(anchor.href, window.location.href);
if (url.origin !== window.location.origin || serverPaths.test(url.pathname)) return;
event.preventDefault();
router.push(url.pathname + url.search + url.hash);
$("#navbarSupportedContent.show").collapse("hide");
};
document.addEventListener("click", navigate);
onBeforeUnmount(() => document.removeEventListener("click", navigate));
return () => [
iconsTemplate(root, []),
h("header", { class: "app-header" }, templates["partials/header.htm"](root, headerCache)),
h("main", { class: "app-view align-items-stretch w-100" }, h(RouterView, null, {
default: ({ Component, route }) => Component ? h(Component, { key: route.meta.preserveExplorer ? route.matched[0]?.path : route.path }) : null,
})),
h("div", { class: "position-fixed p-3", style: { zIndex: 999999999, right: 0, bottom: 0 } }, root.toasts.map((toast, i) => h("div", { class: "toast show", role: "alert", "aria-live": "assertive", "aria-atomic": "true", key: i }, [
h("div", { class: "toast-header" }, [h("strong", { class: "mr-auto" }, toast.title), h("button", { type: "button", class: "ml-2 mb-1 close", "aria-label": "Close", onClick: () => root.removeToast(toast) }, "×")]),
h("div", { class: "toast-body" }, toast.body),
]))),
];
},
});
for (const [name, component] of Object.entries(components)) app.component(name, component);
app.component("Tree", Tree);
app.component("PartialView", {
props: ["name", "state"],
setup(props) { const cache = []; return () => templates[props.name](props.state, cache); },
});
app.directive("field", fieldDirective);
app.directive("form", formDirective);
app.directive("code-editor", codeEditor);
app.directive("paper-scrollspy", paperScrollspy);
app.use(router);
router.beforeEach(async to => {
root?.emit("routeLeave");
if (/^\/(r|repository|anonymize|pull-request-anonymize|gist-anonymize|pr|gist)(\/|$)/.test(to.path)) {
await loadLibrary("markdown");
}
if (/^\/(r|repository)\//.test(to.path)) await loadLibrary("org");
});
router.afterEach(to => {
if (!root) return;
root.title = to.meta.title;
root.emit("routeChange", { title: to.meta.title });
root.emit("routeUpdate", { title: to.meta.title });
const view = document.querySelector(".app-view");
if (!to.meta.preserveExplorer && view) view.scrollTop = 0;
});
app.mount(target);
return { app, router, state: root };
}
if (document.querySelector("#app")) window.anonymousApp = mountApplication();