mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
Viewer: full-height app bar with a Help menu, search + ZIP sidebar, one toolbar for README and code, reading mode with a scroll-tracking outline, docked find for Markdown and code, and redaction slots (a blank stone slot holding the term number) in prose, headings, code, the tree and the path. Landing and FAQ: live redaction demo, serif display headings, teal accent. Author screens: My work table with copy/open/edit actions, a usage meter, a single-container New anonymization flow with a sticky submit bar, themed form controls, and consistent GitHub connection cards. Admin: underline tabs, severity-first errors chart and list, error tiles that filter (older pages load until matches appear), queue and overview health states, and shared toolbar heights and borders. Fixes: guarded localStorage access, failed README images render a placeholder, phone hover states no longer stick, and UI tests updated for the new copy and markup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
5.2 KiB
JavaScript
196 lines
5.2 KiB
JavaScript
function loadFilterPrefs(key) {
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
return raw ? JSON.parse(raw) : null;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function saveFilterPrefs(key, value) {
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify(value));
|
|
} catch (e) {
|
|
/* localStorage unavailable or quota exceeded */
|
|
}
|
|
}
|
|
|
|
function encodePathForUrl(path) {
|
|
return String(path || "")
|
|
.split("/")
|
|
.map((segment) => encodeURIComponent(segment))
|
|
.join("/");
|
|
}
|
|
|
|
function humanFileSize(bytes, si = false, dp = 1) {
|
|
const thresh = si ? 1000 : 1024;
|
|
|
|
bytes = bytes / 8;
|
|
|
|
if (Math.abs(bytes) < thresh) {
|
|
return bytes + " B";
|
|
}
|
|
|
|
const units = si
|
|
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
|
let u = -1;
|
|
const r = 10 ** dp;
|
|
|
|
do {
|
|
bytes /= thresh;
|
|
++u;
|
|
} while (
|
|
Math.round(Math.abs(bytes) * r) / r >= thresh &&
|
|
u < units.length - 1
|
|
);
|
|
|
|
return bytes.toFixed(dp) + " " + units[u];
|
|
}
|
|
|
|
function urlRel2abs(
|
|
url,
|
|
baseUrl = location.href.match(/^(.+)\/?(?:#.+)?$/)[0] + "/"
|
|
) {
|
|
/* Only accept commonly trusted protocols:
|
|
* Only data-image URLs are accepted, Exotic flavours (escaped slash,
|
|
* html-entitied characters) are not supported to keep the function fast.
|
|
* "javascript:" is intentionally NOT allowed — returning such a URL
|
|
* unchanged would let it reach an href attribute and execute on click
|
|
* (XSS, CWE-79). */
|
|
if (
|
|
/^(https?|file|ftps?|mailto|data:image\/[^;]{2,9};):/i.test(url)
|
|
) {
|
|
return url; //Url is already absolute
|
|
}
|
|
// Block any other explicit scheme (javascript:, vbscript:, data:text/html,
|
|
// …) so it can't slip through as an "absolute" URL via the relative-path
|
|
// handling below.
|
|
if (/^\s*[a-z][a-z0-9+.-]*:/i.test(url)) {
|
|
return "";
|
|
}
|
|
|
|
if (url.substring(0, 2) == "//") return location.protocol + url;
|
|
else if (url.charAt(0) == "/") return baseUrl + url;
|
|
// Strip the leading "./" so it concatenates cleanly with baseUrl. The old
|
|
// code prepended an extra "." here, which turned "./X" into "../X" and
|
|
// silently dropped one path segment — see #346.
|
|
else if (url.substring(0, 2) == "./") url = url.substring(2);
|
|
else if (/^\s*$/.test(url)) return "";
|
|
//Empty = Return nothing
|
|
|
|
url = baseUrl + url;
|
|
|
|
while (/\/\.\.\//.test((url = url.replace(/[^\/]+\/+\.\.\//g, ""))));
|
|
/* Escape certain characters to prevent XSS */
|
|
url = url
|
|
.replace(/\.$/, "")
|
|
.replace(/\/\.\//g, "")
|
|
.replace(/"/g, "%22")
|
|
.replace(/'/g, "%27")
|
|
.replace(/</g, "%3C")
|
|
.replace(/>/g, "%3E");
|
|
|
|
return url;
|
|
}
|
|
|
|
const charactersAttributes = "[^-a-z0-9:._]";
|
|
const allTagCharacters = "(?:[^>\"']*(?:\"[^\"]*\"|'[^']*'))*?[^>]*";
|
|
|
|
function by(baseUrl) {
|
|
return (match, group1, group2, group3) => {
|
|
/* Note that this function can also be used to remove links:
|
|
* return group1 + "javascript://" + group3; */
|
|
return group1 + urlRel2abs(group2, baseUrl) + group3;
|
|
};
|
|
}
|
|
|
|
function cr(html, selector, attribute, baseUrl) {
|
|
if (typeof selector == "string") selector = new RegExp(selector, "gi");
|
|
attribute = charactersAttributes + attribute;
|
|
const marker = "\\s*=\\s*";
|
|
const end = ")(";
|
|
var re1 = new RegExp("(" + attribute + marker + '")([^"]+)()', "gi");
|
|
var re2 = new RegExp("(" + attribute + marker + "')([^']+)()", "gi");
|
|
var re3 = new RegExp(
|
|
"(" + attribute + marker + ")([^\"'][^\\s>]*" + end + ")",
|
|
"gi"
|
|
);
|
|
html = html.replace(selector, function (match) {
|
|
return match
|
|
.replace(re1, by(baseUrl))
|
|
.replace(re2, by(baseUrl))
|
|
.replace(re3, by(baseUrl));
|
|
});
|
|
return html;
|
|
}
|
|
|
|
function contentAbs2Relative(content, baseUrl) {
|
|
if (!content) return content;
|
|
content = cr(
|
|
content,
|
|
"<" +
|
|
allTagCharacters +
|
|
charactersAttributes +
|
|
"href\\s*=" +
|
|
allTagCharacters +
|
|
">",
|
|
"href",
|
|
baseUrl
|
|
);
|
|
content = cr(
|
|
content,
|
|
"<" +
|
|
allTagCharacters +
|
|
charactersAttributes +
|
|
"src\\s*=" +
|
|
allTagCharacters +
|
|
">",
|
|
"src",
|
|
baseUrl
|
|
);
|
|
return content;
|
|
}
|
|
|
|
function generateRandomId(length) {
|
|
const alphabet = "ABCDEF0123456789";
|
|
let output = "";
|
|
for (let index = 0; index < length; index++) {
|
|
output += alphabet[Math.round(Math.random() * (alphabet.length - 1))];
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function parseGithubUrl(url) {
|
|
if (!url) throw "Invalid url";
|
|
// Gist URLs: https://gist.github.com/<owner>/<gistId> or
|
|
// https://gist.github.com/<gistId>
|
|
const gistMatch = url.match(
|
|
/gist\.github\.com\/(?:(?<owner>[\w-\._]+)\/)?(?<gist>[a-fA-F0-9]+)/
|
|
);
|
|
if (gistMatch && gistMatch.groups.gist) {
|
|
return {
|
|
owner: gistMatch.groups.owner,
|
|
gistId: gistMatch.groups.gist,
|
|
};
|
|
}
|
|
const matches = url
|
|
.replace(/\.git(\/|$)/, "$1")
|
|
.match(
|
|
/.*?github.com\/(?<owner>[\w-\._]+)\/(?<repo>[\w-\._]+)(\/pull\/(?<PR>[0-9]+))?/
|
|
);
|
|
if (matches && matches.groups.owner && matches.groups.repo) {
|
|
return {
|
|
owner: matches.groups.owner,
|
|
repo: matches.groups.repo,
|
|
pullRequestId: matches.groups.PR,
|
|
};
|
|
} else {
|
|
throw "Invalid url";
|
|
}
|
|
}
|
|
|
|
function renderMD(md, baseUrlValue) {
|
|
return window.MarkdownRenderer.render(md || "", baseUrlValue);
|
|
}
|