mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 08:17:26 +01:00
Backend, scripts, docs and their tests take main's newer code where both sides changed the same lines (the shared blob cache, GitHubStream and markdown fixes landed on main after the Go migration branch was cut). The Dockerfile keeps the Go build from the migration, plus main's owner-alert-protocol label; .gitignore keeps both sides' entries. Frontend keeps the redesign and folds in main's features: Stripe billing in the New anonymization submit bar, GitHub access repair, redaction syntax help, the preview expand toggle, configurable redaction mask, folder counts, reviewer-route header rules, link expiry (now in the viewer's snapshot details) and the explorer error card. main's narrow-only wrap logic is dropped because the redesign wraps on desktop and scrolls sideways on phones. Tests: 1534 passing with SESSION_SECRET set (as in CI). Three main tests were updated for the agreed viewer design (no site header in the viewer, no repository-wide file count). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
6.0 KiB
JavaScript
135 lines
6.0 KiB
JavaScript
import { loadLibrary, loadEditor } from "./lazy-assets.js";
|
|
import { h, ref, watch, onMounted, onBeforeUnmount, nextTick, defineAsyncComponent } from "vue";
|
|
import HtmlDoc from "./html-doc.js";
|
|
import PdfViewer from "./pdf-viewer.js";
|
|
import { render as quotaUsageTemplate } from "../partials/quotaUsage.htm";
|
|
|
|
const QuotaUsage = {
|
|
props: ["quota", "fmt"],
|
|
setup(props) {
|
|
return { formatAmount: (value, kind) => kind === "bytes"
|
|
? props.fmt.humanFileSize(props.quota?.storage?.unit === "bytes" ? value * 8 : value).replace(/([a-zA-Z]+)$/, " $1") : props.fmt.number(value) };
|
|
},
|
|
render: quotaUsageTemplate,
|
|
};
|
|
|
|
const editorModes = new Set(__ACE_MODES__);
|
|
|
|
const Markdown = {
|
|
props: ["content", "terms", "options"],
|
|
setup(props) {
|
|
const host = ref(null);
|
|
const render = () => {
|
|
host.value.innerHTML = renderMD(props.content || "", window.location.pathname + "/../");
|
|
};
|
|
onMounted(() => { render(); watch(() => [props.content, props.terms, props.options], render, { deep: true }); });
|
|
return () => h("markdown", { ref: host });
|
|
},
|
|
};
|
|
const GistFile = {
|
|
props: ["file", "terms", "options"],
|
|
setup(props) {
|
|
const host = ref(null);
|
|
const highlight = () => nextTick(() => host.value?.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el)));
|
|
onMounted(highlight);
|
|
watch(() => [props.file?.content, props.terms, props.options], highlight, { deep: true });
|
|
return () => {
|
|
const file = props.file || {};
|
|
const extension = (file.filename || "").split(".").pop().toLowerCase();
|
|
if (["md", "markdown"].includes(extension) || file.language === "Markdown") {
|
|
return h("gist-file", { ref: host }, h(Markdown, { content: file.content, terms: props.terms, options: props.options }));
|
|
}
|
|
const aliases = { js: "javascript", jsx: "javascript", ts: "javascript", typescript: "javascript", py: "python", html: "markup", xml: "markup", svg: "markup", sh: "bash" };
|
|
const language = (file.language || extension || "none").toLowerCase();
|
|
return h("gist-file", { ref: host }, h("pre", { class: "line-numbers" }, h("code", { class: "language-" + (aliases[language] || language), key: file.content }, file.content || "")));
|
|
};
|
|
},
|
|
};
|
|
const Notebook = {
|
|
props: ["file", "content"],
|
|
setup(props) {
|
|
const host = ref(null);
|
|
let generation = 0;
|
|
let request;
|
|
const render = async () => {
|
|
const current = ++generation;
|
|
request?.abort();
|
|
request = new AbortController();
|
|
try {
|
|
const json = props.content ? JSON.parse(props.content) : await fetch(props.file?.download_url || props.file, { signal: request.signal }).then(r => { if (!r.ok) throw Error("Notebook request failed"); return r.json(); });
|
|
await loadLibrary("notebook");
|
|
if (current !== generation) return;
|
|
host.value.innerHTML = DOMPurify.sanitize(nb.parse(json).render());
|
|
host.value.querySelectorAll("pre code").forEach(el => window.Prism?.highlightElement(el));
|
|
} catch (error) { if (current === generation && error.name !== "AbortError") host.value.textContent = "Unable to render the notebook."; }
|
|
};
|
|
onMounted(() => { render(); watch(() => [props.file, props.content], render); });
|
|
onBeforeUnmount(() => { generation++; request?.abort(); });
|
|
return () => h("notebook", { ref: host });
|
|
},
|
|
};
|
|
const Loc = {
|
|
props: ["stats"],
|
|
setup(props) {
|
|
return () => {
|
|
const rows = Object.entries(props.stats || {}).filter(([, stat]) => stat.code);
|
|
const total = rows.reduce((sum, [, stat]) => sum + stat.code, 0);
|
|
return h("loc", rows.map(([language, stat]) => h("div", { class: "lang", title: `${language}: ${stat.code.toLocaleString()} lines`, style: { width: stat.code * 100 / total + "%", background: langColors[language] } })));
|
|
};
|
|
},
|
|
};
|
|
export const components = { Markdown, GistFile, Notebook, Loc, HtmlDoc, QuotaUsage, Pdfviewer: defineAsyncComponent(async () => {
|
|
await loadLibrary("pdf");
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = "/script/external/pdf.worker.js";
|
|
return PdfViewer;
|
|
}) };
|
|
|
|
export const codeEditor = {
|
|
async mounted(el, { value }) {
|
|
el._editorValue = value;
|
|
try {
|
|
await loadEditor();
|
|
if (el._editorDisposed) return;
|
|
const latest = el._editorValue;
|
|
const editor = ace.edit(el);
|
|
el._editor = editor;
|
|
editor.setValue(String(latest.content ?? ""), -1);
|
|
applyEditorOptions(el, latest.options);
|
|
el._editorCleanup = latest.options?.onLoad?.(editor);
|
|
} catch (error) {
|
|
if (!el._editorDisposed) el.textContent = error.message;
|
|
}
|
|
},
|
|
updated(el, { value }) {
|
|
el._editorValue = value;
|
|
if (!el._editor) return;
|
|
if (el._editor.getValue() !== String(value.content ?? "")) el._editor.setValue(String(value.content ?? ""), -1);
|
|
applyEditorOptions(el, value.options);
|
|
},
|
|
beforeUnmount(el) { el._editorDisposed = true; el._editorCleanup?.(); el._editor?.destroy(); },
|
|
};
|
|
function applyEditorOptions(el, options = {}) {
|
|
const mode = editorModes.has(options.mode) ? options.mode : "text";
|
|
el._editor.session.setMode("ace/mode/" + mode);
|
|
if (options.theme) el._editor.setTheme("ace/theme/" + options.theme);
|
|
el._editor.setReadOnly(options.readOnly !== false);
|
|
}
|
|
export const paperScrollspy = {
|
|
mounted(el) {
|
|
if (!window.IntersectionObserver) return;
|
|
const links = [...el.querySelectorAll('a[href^="#"]')];
|
|
const visible = new Set();
|
|
const observer = new IntersectionObserver(entries => {
|
|
entries.forEach(e => e.isIntersecting ? visible.add(e.target.id) : visible.delete(e.target.id));
|
|
const active = links.find(a => visible.has(a.hash.slice(1)));
|
|
if (active) links.forEach(a => a.classList.toggle("active", a === active));
|
|
}, { rootMargin: "-20% 0px -60% 0px", threshold: 0 });
|
|
nextTick(() => {
|
|
links.forEach(a => { const target = document.getElementById(a.hash.slice(1)); if (target) observer.observe(target); });
|
|
if (links[0] && !links.some(a => a.classList.contains("active"))) links[0].classList.add("active");
|
|
});
|
|
el._observer = observer;
|
|
},
|
|
beforeUnmount(el) { el._observer?.disconnect(); },
|
|
};
|