mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-19 14:17:20 +02:00
fix: keep redaction marks correct with custom masks, Prism and Mermaid
- The mask from /api/options is stored once loaded; code markers and rendered Markdown are refreshed when it arrives instead of keeping the XXXX default. - Mermaid sources are left untouched so diagrams still parse. - Fenced code blocks are marked from a Prism complete hook, after Prism has rewritten their HTML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
80944954d6
commit
be2f979296
6 files changed
+71
-29
No files matched your search
+36
-11
@@ -2,25 +2,34 @@ import { reactive, nextTick } from "vue";
|
||||
import { createTimers, createListeners } from "./state.js";
|
||||
import { formDraft } from "./drafts.js";
|
||||
|
||||
// The mask comes from /api/options; mainController sets it once loaded.
|
||||
let redactionMask = "XXXX";
|
||||
export const setRedactionMask = (mask) => { redactionMask = mask || "XXXX"; };
|
||||
|
||||
// Matches the replacements the anonymizer writes: MASK and MASK-<n>.
|
||||
export const redactionPattern = (mask = "XXXX") => {
|
||||
export const redactionPattern = (mask = redactionMask) => {
|
||||
if (!mask) return null;
|
||||
const escaped = mask.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
return new RegExp(escaped + "(?:-\\d+)?", "g");
|
||||
}
|
||||
};
|
||||
|
||||
// Wraps redacted terms in rendered HTML with <mark class="redaction">,
|
||||
// touching text nodes only so attributes and URLs stay intact.
|
||||
export const highlightRedactions = (html, mask = "XXXX") => {
|
||||
// Wraps redacted terms under root in <mark class="redaction">, touching text
|
||||
// nodes only so attributes and URLs stay intact. Mermaid sources are left
|
||||
// alone (Mermaid parses their text), and so are code blocks Prism will
|
||||
// rewrite unless root is that highlighted block itself.
|
||||
export const markRedactionsIn = (root, mask = redactionMask) => {
|
||||
const pattern = redactionPattern(mask);
|
||||
if (!pattern || !html || !html.includes(mask)) return html;
|
||||
const doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
|
||||
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
||||
if (!pattern || !root || !root.textContent.includes(mask)) return;
|
||||
const doc = root.ownerDocument;
|
||||
const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
const nodes = [];
|
||||
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||
for (const node of nodes) {
|
||||
const text = node.nodeValue;
|
||||
if (!text.includes(mask) || node.parentElement?.closest("script, style")) continue;
|
||||
const parent = node.parentElement;
|
||||
if (!text.includes(mask) || !parent || parent.closest("script, style, mark.redaction, .mermaid")) continue;
|
||||
const highlighted = parent.closest('code[class*="language-"]');
|
||||
if (highlighted && !highlighted.contains(root)) continue;
|
||||
const fragment = doc.createDocumentFragment();
|
||||
let last = 0;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
@@ -35,8 +44,14 @@ export const highlightRedactions = (html, mask = "XXXX") => {
|
||||
fragment.append(text.slice(last));
|
||||
node.replaceWith(fragment);
|
||||
}
|
||||
};
|
||||
|
||||
export const highlightRedactions = (html, mask = redactionMask) => {
|
||||
if (!html || !mask || !html.includes(mask)) return html;
|
||||
const doc = new DOMParser().parseFromString("<body>" + html + "</body>", "text/html");
|
||||
markRedactionsIn(doc.body, mask);
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
async function copyText(text) {
|
||||
if (window.navigator.clipboard?.writeText) {
|
||||
@@ -147,6 +162,8 @@ export const mainController = function (state, http, location, timeout) {
|
||||
http.get("/api/options").then(
|
||||
(res) => {
|
||||
if (res) state.site_options = res.data;
|
||||
setRedactionMask(res?.data?.ANONYMIZATION_MASK);
|
||||
state.emit("site-options");
|
||||
},
|
||||
() => {
|
||||
state.site_options = null;
|
||||
@@ -2202,6 +2219,11 @@ export const anonymizeController = function (state, http, html, params, location
|
||||
|
||||
export const exploreController = function (state, http, location, params, html, promises) {
|
||||
const timers = createTimers();
|
||||
// Prism rewrites fenced code blocks, so mark redactions once it is done.
|
||||
if (window.Prism?.hooks && !window.Prism.redactionHook) {
|
||||
window.Prism.hooks.add("complete", (env) => markRedactionsIn(env.element));
|
||||
window.Prism.redactionHook = true;
|
||||
}
|
||||
const listen = createListeners();
|
||||
state.on("dark-mode", (event, on) => {
|
||||
if (!state.aceOption) return;
|
||||
@@ -2808,7 +2830,7 @@ export const exploreController = function (state, http, location, params, html,
|
||||
const markRedactions = () => {
|
||||
redactionMarkers.forEach((id) => _editor.session.removeMarker(id));
|
||||
redactionMarkers = [];
|
||||
const pattern = redactionPattern(state.site_options?.ANONYMIZATION_MASK);
|
||||
const pattern = redactionPattern();
|
||||
if (!pattern) return;
|
||||
const lines = _editor.session.getDocument().getAllLines();
|
||||
lines.forEach((line, row) => {
|
||||
@@ -2823,8 +2845,11 @@ export const exploreController = function (state, http, location, params, html,
|
||||
};
|
||||
markRedactions();
|
||||
_editor.session.on("change", () => timers.timeout(markRedactions, 0));
|
||||
// The mask may arrive after the editor mounts.
|
||||
const offOptions = state.on("site-options", markRedactions);
|
||||
return () => {
|
||||
removeHashListener();
|
||||
offOptions();
|
||||
narrow.removeEventListener?.("change", applyWrap);
|
||||
};
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user