Markdown / static /script.js
GauthierDemonchy
Premier commit propre
898ed62
Raw
History Blame Contribute Delete
23.1 kB
/* ============================
MarkitDown — Script Frontend
============================ */
let allResults = [];
// Liste des catégories et extensions associées
const supportedFormats = {
"Documents Office": [".docx", ".pptx", ".xlsx"],
"PDF": [".pdf"],
"Images": [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"],
"Web": [".html", ".htm"],
"Données": [".csv", ".json", ".xml"],
"Archives": [".zip"],
"Texte": [".txt", ".md", ".rst"],
"Audio": [".mp3", ".wav"]
};
// Éléments DOM
const uploadZone = document.getElementById("upload-zone");
const fileInput = document.getElementById("file-input");
const uploadStatus = document.getElementById("upload-status");
const resultSection = document.getElementById("result-section");
const resultCount = document.getElementById("result-count");
const resultList = document.getElementById("result-list");
const btnDownloadZip = document.getElementById("btn-download-zip");
const btnCopyAll = document.getElementById("btn-copy-all");
const historyList = document.getElementById("history-list");
const historyPanel = document.getElementById("history-panel");
const toggleHistory = document.getElementById("toggle-history");
const btnClearHistory = document.getElementById("btn-clear-history");
const filterCheckboxesContainer = document.getElementById("filter-checkboxes");
const btnFilterSelectAll = document.getElementById("filter-select-all");
const btnFilterDeselectAll = document.getElementById("filter-deselect-all");
// ============================
// Initialisation des filtres
// ============================
function initFilters() {
Object.entries(supportedFormats).forEach(([category, extensions]) => {
const label = document.createElement("label");
label.className = "filter-label";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "filter-checkbox";
checkbox.checked = true; // Tout coché par défaut
checkbox.dataset.extensions = extensions.join(",");
label.appendChild(checkbox);
label.appendChild(document.createTextNode(`${category} (${extensions.join(", ")})`));
filterCheckboxesContainer.appendChild(label);
});
btnFilterSelectAll.addEventListener("click", () => {
document.querySelectorAll(".filter-checkbox").forEach(cb => cb.checked = true);
});
btnFilterDeselectAll.addEventListener("click", () => {
document.querySelectorAll(".filter-checkbox").forEach(cb => cb.checked = false);
});
}
function getSelectedExtensions() {
const extensions = new Set();
document.querySelectorAll(".filter-checkbox:checked").forEach(cb => {
cb.dataset.extensions.split(",").forEach(ext => extensions.add(ext));
});
return extensions;
}
initFilters();
// ============================
// Upload — Drag & Drop + Click
// ============================
uploadZone.addEventListener("click", () => fileInput.click());
uploadZone.addEventListener("dragover", (e) => {
e.preventDefault();
uploadZone.classList.add("drag-over");
});
uploadZone.addEventListener("dragleave", () => {
uploadZone.classList.remove("drag-over");
});
uploadZone.addEventListener("drop", async (e) => {
e.preventDefault();
uploadZone.classList.remove("drag-over");
console.log("--- DEBUG: Drop event triggered ---");
const items = e.dataTransfer.items;
// Détection de l'incompatibilité (Firefox) pour le glisser-déposer de dossiers
if (items && items.length > 0 && typeof items[0].webkitGetAsEntry === "undefined") {
let hasDirectory = false;
// Sur Firefox, un dossier déposé n'a pas de type et une taille de 0
for (let i = 0; i < e.dataTransfer.files.length; i++) {
if (e.dataTransfer.files[i].size === 0 && e.dataTransfer.files[i].type === "") {
hasDirectory = true;
break;
}
}
if (hasDirectory) {
uploadStatus.textContent = "Le glisser-déposer de dossiers n'est pas supporté sur ce navigateur. Veuillez cliquer pour sélectionner un dossier.";
uploadStatus.className = "upload-status error";
console.log("DEBUG: Folder drop on incompatible browser (like Firefox) detected. Aborting.");
return;
}
}
if (items && items.length > 0) {
console.log(`DEBUG: ${items.length} items detected in drop event.`);
const files = [];
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
const entry = items[i].webkitGetAsEntry();
if (entry) {
console.log(`DEBUG: Processing item #${i+1}: ${entry.name}`);
await traverseFileTree(entry, "", files);
}
}
}
console.log(`DEBUG: Finished traversing all items. Total files found: ${files.length}`);
if (files.length > 0) {
uploadFiles(files);
} else {
console.log("DEBUG: No files found after traversal.");
}
} else {
console.log("DEBUG: Drop event with no items, trying e.dataTransfer.files.");
const files = e.dataTransfer.files;
if (files.length > 0) {
uploadFiles(Array.from(files));
}
}
});
async function traverseFileTree(item, path, filesArray) {
path = path || "";
if (item.isFile) {
console.log(`DEBUG: Found file: ${path}${item.name}`);
await new Promise((resolve, reject) => {
item.file(file => {
if (!file.name.startsWith('.')) {
Object.defineProperty(file, 'fullPath', {
value: path + item.name,
writable: false
});
filesArray.push(file);
}
resolve();
}, reject);
});
} else if (item.isDirectory) {
console.log(`DEBUG: Entering directory: ${path}${item.name}`);
const dirReader = item.createReader();
const readAllDirectoryEntries = async (directoryReader) => {
const entries = [];
let readEntries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});
while (readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});
}
return entries;
};
try {
const allEntries = await readAllDirectoryEntries(dirReader);
console.log(`DEBUG: Found ${allEntries.length} entries in directory ${item.name}`);
for (const entry of allEntries) {
await traverseFileTree(entry, path + item.name + "/", filesArray);
}
} catch (error) {
console.error("DEBUG: Error reading directory:", error);
}
}
}
fileInput.addEventListener("change", () => {
if (fileInput.files.length > 0) {
console.log("--- DEBUG: File input change event triggered ---");
uploadFiles(Array.from(fileInput.files));
fileInput.value = "";
}
});
// ============================
// Upload & Conversion
// ============================
async function uploadFiles(files) {
console.log("--- DEBUG: uploadFiles function called ---");
const selectedExtensions = getSelectedExtensions();
if (selectedExtensions.size === 0) {
uploadStatus.textContent = "Erreur : Veuillez sélectionner au moins un type de fichier à traiter.";
uploadStatus.className = "upload-status error";
return;
}
const filteredFiles = files.filter(file => {
const fileName = file.fullPath || file.webkitRelativePath || file.name;
const match = fileName.match(/\.[0-9a-z]+$/i);
if (!match) return false;
return selectedExtensions.has(match[0].toLowerCase());
});
if (filteredFiles.length === 0) {
uploadStatus.textContent = "Aucun fichier correspondant aux filtres sélectionnés n'a été trouvé.";
uploadStatus.className = "upload-status error";
console.log("DEBUG: No files left after filtering by extension.");
return;
}
console.log(`DEBUG: ${filteredFiles.length} files left after filtering.`);
uploadStatus.textContent = `Conversion de ${filteredFiles.length} fichier(s) en cours…`;
uploadStatus.className = "upload-status";
const formData = new FormData();
console.log("DEBUG: Building FormData to send to server...");
for (const file of filteredFiles) {
let fileName = file.fullPath || file.webkitRelativePath || file.name;
formData.append("files", file);
formData.append("paths", fileName);
console.log(`DEBUG: Appending to FormData: file=${file.name}, path=${fileName}`);
}
try {
console.log("DEBUG: Sending request to /upload");
const response = await fetch("/upload", {
method: "POST",
body: formData,
});
console.log(`DEBUG: Received response from server with status: ${response.status}`);
if (!response.ok) {
if (response.status === 413) {
uploadStatus.textContent = "Erreur : Le dossier ou les fichiers sont trop volumineux (Limite dépassée).";
uploadStatus.className = "upload-status error";
return;
}
}
const data = await response.json();
console.log("DEBUG: Server response JSON:", data);
if (!data.success) {
uploadStatus.textContent = data.error || "Erreur lors de la conversion.";
uploadStatus.className = "upload-status error";
return;
}
if (data.errors && data.errors.length > 0) {
const errorMsgs = data.errors.map((e) => `${e.filename}: ${e.error}`).join("; ");
uploadStatus.textContent = `Erreurs: ${errorMsgs}`;
uploadStatus.className = "upload-status error";
} else {
uploadStatus.textContent = `${data.results.length} fichier(s) converti(s) avec succès.`;
uploadStatus.className = "upload-status success";
}
if (data.results && data.results.length > 0) {
displayResults(data.results);
}
loadHistory();
} catch (err) {
console.error("DEBUG: Network or fetch error:", err);
uploadStatus.textContent = "Erreur réseau. Vérifiez que le serveur est lancé et que le fichier/dossier n'est pas trop lourd (413).";
uploadStatus.className = "upload-status error";
}
}
// ============================
// Affichage des résultats (multi-fichier)
// ============================
function displayResults(results) {
allResults = results;
resultCount.textContent = `${results.length} fichier(s) converti(s)`;
resultList.innerHTML = "";
results.forEach((result, index) => {
const item = document.createElement("div");
item.className = "result-item";
item.dataset.id = result.id;
const uid = result.id || `result-${index}`;
item.innerHTML = `
<div class="result-item-header" data-target="${uid}">
<span class="result-item-filename">
${escapeHtml(result.filename)}
<span class="file-size">${formatSize(result.size)}</span>
</span>
<span class="collapse-icon">&#9660;</span>
</div>
<div class="result-item-body" id="body-${uid}">
<div class="result-item-tabs">
<button class="tab-button active" data-tab="rendered-${uid}" type="button">Rendu</button>
<button class="tab-button" data-tab="raw-${uid}" type="button">Markdown brut</button>
</div>
<div class="result-body-content">
<div id="tab-rendered-${uid}" class="tab-content active rendered-preview">${renderMarkdown(result.markdown)}</div>
<pre id="tab-raw-${uid}" class="tab-content raw-preview">${escapeHtml(result.markdown)}</pre>
</div>
<div class="result-item-actions">
<button class="btn-small btn-primary item-download" data-id="${result.id}" data-filename="${escapeHtml(result.filename)}" type="button">&#11015; Télécharger</button>
<button class="btn-small btn-secondary item-copy" data-id="${result.id}" type="button">&#128203; Copier</button>
</div>
</div>
`;
resultList.appendChild(item);
});
resultSection.classList.remove("hidden");
attachResultEvents();
}
function attachResultEvents() {
document.querySelectorAll(".result-item-header").forEach((header) => {
header.addEventListener("click", () => {
const targetId = header.dataset.target;
const body = document.getElementById(`body-${targetId}`);
const icon = header.querySelector(".collapse-icon");
body.classList.toggle("hidden");
icon.classList.toggle("collapsed");
});
});
document.querySelectorAll(".result-item .tab-button").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const parent = btn.closest(".result-item");
parent.querySelectorAll(".tab-button").forEach((b) => b.classList.remove("active"));
parent.querySelectorAll(".tab-content").forEach((el) => el.classList.remove("active"));
btn.classList.add("active");
const tabId = btn.dataset.tab;
parent.querySelector(`#tab-${tabId}`).classList.add("active");
});
});
document.querySelectorAll(".item-download").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const filename = btn.dataset.filename;
downloadSingle(id, filename);
});
});
document.querySelectorAll(".item-copy").forEach((btn) => {
btn.addEventListener("click", async (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const result = allResults.find((r) => r.id === id);
if (result) {
try {
await navigator.clipboard.writeText(result.markdown);
showToast("Copié dans le presse-papier");
} catch {
showToast("Erreur de copie");
}
}
});
});
}
function getSafeFilename(originalFilename) {
const parts = originalFilename.split(/[/\\]/);
const baseName = parts[parts.length - 1];
return baseName.replace(/\.[^.]+$/, "") + ".md";
}
function downloadSingle(id, filename) {
const result = allResults.find((r) => r.id === id);
if (!result) return;
const safeName = getSafeFilename(filename);
const blob = new Blob([result.markdown], { type: "text/markdown" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = safeName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast("Fichier téléchargé");
}
// ============================
// Télécharger tout (ZIP)
// ============================
function loadJSZip() {
return new Promise((resolve, reject) => {
if (window.JSZip) {
resolve(window.JSZip);
return;
}
const script = document.createElement('script');
script.src = "https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js";
script.onload = () => resolve(window.JSZip);
script.onerror = reject;
document.head.appendChild(script);
});
}
btnDownloadZip.addEventListener("click", async () => {
if (allResults.length === 0) return;
try {
const JSZip = await loadJSZip();
const zip = new JSZip();
allResults.forEach((r) => {
let pathName = r.filename.replace(/\\/g, "/");
pathName = pathName.replace(/\.[^.]+$/, "") + ".md";
if(pathName) {
zip.file(pathName, r.markdown);
}
});
const content = await zip.generateAsync({type: "blob"});
const url = URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = "MarkitDown_Results.zip";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showToast(`Archive de ${allResults.length} fichier(s) téléchargée`);
} catch (e) {
showToast("Erreur lors de la création du ZIP");
console.error(e);
}
});
// ============================
// Copier tout
// ============================
btnCopyAll.addEventListener("click", async () => {
if (allResults.length === 0) return;
const combined = allResults
.map((r) => `# ${r.filename}\n\n${r.markdown}`)
.join("\n\n---\n\n");
try {
await navigator.clipboard.writeText(combined);
showToast("Tout copié dans le presse-papier");
} catch {
showToast("Erreur de copie");
}
});
// ============================
// Historique
// ============================
toggleHistory.addEventListener("click", () => {
historyPanel.classList.toggle("hidden");
if (!historyPanel.classList.contains("hidden")) {
loadHistory();
}
});
btnClearHistory.addEventListener("click", async () => {
try {
await fetch("/clear-history", { method: "POST" });
loadHistory();
showToast("Historique effacé");
} catch {
showToast("Erreur lors de l'effacement");
}
});
async function loadHistory() {
try {
const response = await fetch("/history");
const data = await response.json();
if (data.success) {
renderHistory(data.data);
}
} catch {
// Silencieux
}
}
function renderHistory(entries) {
if (!entries || entries.length === 0) {
historyList.innerHTML = '<p class="history-empty">Aucune conversion pour le moment.</p>';
return;
}
historyList.innerHTML = entries
.map(
(entry, index) => `
<div class="history-item" data-id="${entry.id}" data-index="${index}">
<div class="history-item-info">
<div class="history-item-name">${escapeHtml(entry.filename)}</div>
<div class="history-item-date">${escapeHtml(entry.date)} &middot; ${formatSize(entry.size)}</div>
</div>
<div class="history-item-actions">
<button class="btn-icon history-download" data-id="${entry.id}" title="Télécharger">&#11015;</button>
<button class="btn-icon history-copy" data-id="${entry.id}" title="Copier">&#128203;</button>
</div>
</div>
`
)
.join("");
historyList.querySelectorAll(".history-download").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
downloadHistoryEntry(btn.dataset.id);
});
});
historyList.querySelectorAll(".history-copy").forEach((btn) => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
copyHistoryEntry(btn.dataset.id);
});
});
historyList.querySelectorAll(".history-item").forEach((item) => {
item.addEventListener("click", () => {
const entry = entries[item.dataset.index];
if (entry) displayResults([entry]);
});
});
}
async function downloadHistoryEntry(id) {
try {
const response = await fetch(`/download/${id}`);
if (!response.ok) {
showToast("Erreur de téléchargement");
return;
}
const blob = await response.blob();
const disposition = response.headers.get("Content-Disposition") || "";
const match = disposition.match(/filename[^;=\n]*=([^"'\n]*)/);
const filename = match ? match[1].trim() : "document.md";
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch {
showToast("Erreur de téléchargement");
}
}
async function copyHistoryEntry(id) {
try {
const response = await fetch(`/copy/${id}`);
const data = await response.json();
if (data.success) {
await navigator.clipboard.writeText(data.markdown);
showToast("Copié dans le presse-papier");
}
} catch {
showToast("Erreur de copie");
}
}
// ============================
// Rendu Markdown basique
// ============================
function renderMarkdown(md) {
let html = md
.replace(/```(\w*)\n([\s\S]*?)```/g, "<pre><code>$2</code></pre>")
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/^###### (.+)$/gm, "<h6>$1</h6>")
.replace(/^##### (.+)$/gm, "<h5>$1</h5>")
.replace(/^#### (.+)$/gm, "<h4>$1</h4>")
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>")
.replace(/\*\*\*(.+?)\*\*\*/g, "<strong><em>$1</em></strong>")
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>")
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">')
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
.replace(/^---$/gm, "<hr>")
.replace(/\n\n/g, "</p><p>")
.replace(/\n/g, "<br>");
return "<p>" + html + "</p>";
}
// ============================
// Utilitaires
// ============================
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
function formatSize(bytes) {
if (bytes < 1024) return `${bytes} o`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} Ko`;
return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`;
}
function showToast(message) {
let toast = document.querySelector(".toast");
if (!toast) {
toast = document.createElement("div");
toast.className = "toast";
document.body.appendChild(toast);
}
toast.textContent = message;
toast.classList.add("visible");
clearTimeout(toast._timeout);
toast._timeout = setTimeout(() => {
toast.classList.remove("visible");
}, 2000);
}
loadHistory();