good presidio

This commit is contained in:
nBiqoz
2025-06-18 13:45:23 +02:00
parent 4c0622e47e
commit 8c7a19e881
8 changed files with 898 additions and 469 deletions

View File

@@ -1,172 +0,0 @@
// app/components/MarkdownModal.tsx
import { X, Download } from "lucide-react";
import { jsPDF } from "jspdf";
import { useState } from "react";
import { type PageObject } from "../page"; // Importer le type depuis la page principale
interface HtmlModalProps {
content: PageObject[] | null; // Utilise le type PageObject importé
onClose: () => void;
}
// Moteur de rendu HTML vers PDF, robuste et récursif
const renderHtmlOnPdfPage = (pdf: jsPDF, htmlContent: string) => {
const pageHeight = pdf.internal.pageSize.getHeight();
const pageWidth = pdf.internal.pageSize.getWidth();
const margin = 15;
let cursorY = margin;
const container = document.createElement("div");
container.innerHTML = htmlContent;
const processNode = (
node: ChildNode,
currentStyle: { fontStyle: "normal" | "bold" | "italic"; fontSize: number }
) => {
if (cursorY > pageHeight - margin) {
pdf.addPage();
cursorY = margin;
}
const nextStyle = { ...currentStyle };
let spacingAfter = 0;
switch (node.nodeName) {
case "H1":
nextStyle.fontSize = 22;
nextStyle.fontStyle = "bold";
spacingAfter = 8;
break;
case "H2":
nextStyle.fontSize = 18;
nextStyle.fontStyle = "bold";
spacingAfter = 6;
break;
case "H3":
nextStyle.fontSize = 14;
nextStyle.fontStyle = "bold";
spacingAfter = 4;
break;
case "P":
nextStyle.fontSize = 10;
nextStyle.fontStyle = "normal";
spacingAfter = 4;
break;
case "UL":
spacingAfter = 4;
break;
case "STRONG":
case "B":
nextStyle.fontStyle = "bold";
break;
}
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
pdf.setFontSize(nextStyle.fontSize);
pdf.setFont("Helvetica", nextStyle.fontStyle);
const textLines = node.textContent.split("\n");
textLines.forEach((line) => {
if (line.trim() === "") {
cursorY += nextStyle.fontSize * 0.4;
return;
}
const splitText = pdf.splitTextToSize(line, pageWidth - margin * 2);
pdf.text(splitText, margin, cursorY);
cursorY += splitText.length * nextStyle.fontSize * 0.4;
});
} else if (node.nodeName === "UL") {
Array.from((node as HTMLUListElement).children).forEach((li) => {
const liText = `${li.textContent?.trim() || ""}`;
const lines = pdf.splitTextToSize(liText, pageWidth - margin * 2 - 5);
pdf.setFontSize(10);
pdf.setFont("Helvetica", "normal");
pdf.text(lines, margin + 5, cursorY);
cursorY += lines.length * 10 * 0.4 + 2;
});
}
if (node.childNodes.length > 0) {
node.childNodes.forEach((child) => processNode(child, nextStyle));
}
cursorY += spacingAfter;
};
container.childNodes.forEach((node) =>
processNode(node, { fontStyle: "normal", fontSize: 10 })
);
};
export default function MarkdownModal({ content, onClose }: HtmlModalProps) {
const [isDownloading, setIsDownloading] = useState(false);
if (!content) return null;
const handleDownloadPdf = async () => {
if (!content || content.length === 0) return;
setIsDownloading(true);
try {
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
pdf.setFont("Helvetica", "normal");
// Mis à jour pour boucler sur les objets et passer le contenu HTML
content.forEach((page, index) => {
if (index > 0) pdf.addPage();
renderHtmlOnPdfPage(pdf, page.htmlContent);
});
pdf.save("document_anonymise.pdf");
} catch (error) {
console.error("Erreur lors de la génération du PDF:", error);
} finally {
setIsDownloading(false);
}
};
// Mis à jour pour extraire le contenu HTML de chaque page avant de l'afficher
const previewHtml = content
.map((page) => page.htmlContent)
.join(
'<hr style="margin: 2.5rem 0; border-style: dashed; border-color: rgba(255,255,255,0.2);">'
);
return (
<div
className="fixed inset-0 bg-black bg-opacity-70 z-50 flex justify-center items-center p-4"
onClick={onClose}
>
<div
className="bg-[#061717] border-4 border-white shadow-[10px_10px_0_0_black] w-full max-w-3xl h-[80vh] flex flex-col relative"
onClick={(e) => e.stopPropagation()}
>
<header className="flex items-center justify-between p-4 border-b-4 border-white">
<h3 className="text-lg font-black text-white uppercase">
Aperçu du Document
</h3>
<div className="flex gap-2">
<button
onClick={handleDownloadPdf}
disabled={isDownloading}
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5 disabled:opacity-50"
>
{isDownloading ? (
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : (
<Download className="h-5 w-5" />
)}
</button>
<button
onClick={onClose}
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
>
<X className="h-5 w-5" />
</button>
</div>
</header>
<div
className="preview-content p-6 overflow-y-auto w-full h-full"
dangerouslySetInnerHTML={{ __html: previewHtml }}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,155 @@
import { X, Download } from "lucide-react";
import { jsPDF } from "jspdf";
import { useState } from "react";
interface PresidioModalProps {
anonymizedText: string | null;
piiCount: number;
onClose: () => void;
}
const renderAnonymizedTextOnPdf = (pdf: jsPDF, anonymizedText: string) => {
const pageHeight = pdf.internal.pageSize.getHeight();
const pageWidth = pdf.internal.pageSize.getWidth();
const margin = 15;
let cursorY = margin;
const paragraphs = anonymizedText.split("\n\n").filter((p) => p.trim());
paragraphs.forEach((paragraph) => {
if (cursorY > pageHeight - margin - 20) {
pdf.addPage();
cursorY = margin;
}
const parts = paragraph.split(/(<[A-Z_]+>)/g);
parts.forEach((part) => {
if (part.match(/<[A-Z_]+>/)) {
pdf.setFontSize(10);
pdf.setFont("Helvetica", "bold");
pdf.setTextColor(255, 0, 0);
const lines = pdf.splitTextToSize(part, pageWidth - margin * 2);
pdf.text(lines, margin, cursorY);
cursorY += lines.length * 10 * 0.5;
} else if (part.trim()) {
pdf.setFontSize(10);
pdf.setFont("Helvetica", "normal");
pdf.setTextColor(0, 0, 0);
const lines = pdf.splitTextToSize(part, pageWidth - margin * 2);
pdf.text(lines, margin, cursorY);
cursorY += lines.length * 10 * 0.5;
}
});
cursorY += 8;
});
};
const formatAnonymizedTextForPreview = (text: string): string => {
return text
.split("\n\n")
.map((paragraph) => {
const formattedParagraph = paragraph.replace(
/<([A-Z_]+)>/g,
'<span style="background-color: #ff4444; color: white; padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 0.9em;">&lt;$1&gt;</span>'
);
return `<p style="margin-bottom: 1rem; line-height: 1.6;">${formattedParagraph}</p>`;
})
.join("");
};
export default function PresidioModal({
anonymizedText,
piiCount,
onClose,
}: PresidioModalProps) {
const [isDownloading, setIsDownloading] = useState(false);
if (!anonymizedText) return null;
const handleDownloadPdf = async () => {
setIsDownloading(true);
try {
const pdf = new jsPDF({ orientation: "p", unit: "mm", format: "a4" });
pdf.setFontSize(16);
pdf.setFont("Helvetica", "bold");
pdf.setTextColor(0, 0, 0);
pdf.text("Document Anonymisé par Presidio", 15, 20);
pdf.setFontSize(10);
pdf.setFont("Helvetica", "normal");
pdf.text(
`Données personnelles détectées et anonymisées : ${piiCount}`,
15,
30
);
pdf.setDrawColor(0, 0, 0);
pdf.line(15, 35, pdf.internal.pageSize.getWidth() - 15, 35);
pdf.setFontSize(10);
renderAnonymizedTextOnPdf(pdf, anonymizedText);
pdf.save("document_anonymise_presidio.pdf");
} catch (error) {
console.error("Erreur lors de la génération du PDF:", error);
} finally {
setIsDownloading(false);
}
};
const previewHtml = `
<div style="font-family: Arial, sans-serif; color: white;">
<h2 style="color: #F7AB6E; margin-bottom: 1rem;">Document Anonymisé par Presidio</h2>
<p style="color: #ccc; margin-bottom: 2rem; font-size: 0.9em;">
<strong>${piiCount}</strong> données personnelles détectées et anonymisées
</p>
<div style="border-top: 2px dashed rgba(255,255,255,0.3); padding-top: 1.5rem;">
${formatAnonymizedTextForPreview(anonymizedText)}
</div>
</div>
`;
return (
<div
className="fixed inset-0 bg-black bg-opacity-70 z-50 flex justify-center items-center p-4"
onClick={onClose}
>
<div
className="bg-[#061717] border-4 border-white shadow-[10px_10px_0_0_black] w-full max-w-4xl h-[80vh] flex flex-col relative"
onClick={(e) => e.stopPropagation()}
>
<header className="flex items-center justify-between p-4 border-b-4 border-white">
<h3 className="text-lg font-black text-white uppercase">
Document Anonymisé - {piiCount} données masquées
</h3>
<div className="flex gap-2">
<button
onClick={handleDownloadPdf}
disabled={isDownloading}
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5 disabled:opacity-50"
>
{isDownloading ? (
<div className="h-5 w-5 animate-spin rounded-full border-2 border-white border-t-transparent" />
) : (
<Download className="h-5 w-5" />
)}
</button>
<button
onClick={onClose}
className="p-2 bg-[#F7AB6E] text-white border-2 border-white shadow-[3px_3px_0_0_black] hover:shadow-[1px_1px_0_0_black] active:shadow-none active:translate-x-0.5 active:translate-y-0.5"
>
<X className="h-5 w-5" />
</button>
</div>
</header>
<div
className="preview-content p-6 overflow-y-auto w-full h-full"
dangerouslySetInnerHTML={{ __html: previewHtml }}
/>
</div>
</div>
);
}