Spaces:
Sleeping
Sleeping
| /* ============================ | |
| 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">▼</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">⬇ Télécharger</button> | |
| <button class="btn-small btn-secondary item-copy" data-id="${result.id}" type="button">📋 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)} · ${formatSize(entry.size)}</div> | |
| </div> | |
| <div class="history-item-actions"> | |
| <button class="btn-icon history-download" data-id="${entry.id}" title="Télécharger">⬇</button> | |
| <button class="btn-icon history-copy" data-id="${entry.id}" title="Copier">📋</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(); |