All checks were successful
Deploy MinIO Explorer / deploy (push) Successful in 47s
1111 lines
31 KiB
JavaScript
1111 lines
31 KiB
JavaScript
// Configuration globale
|
|
let config = {
|
|
baseUrl: "",
|
|
currentPath: "",
|
|
};
|
|
|
|
// Initialisation de l'application
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
initializeApp();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function initializeApp() {
|
|
// Tentative de configuration automatique depuis l'URL courante
|
|
autoConfigureFromURL();
|
|
|
|
// Affichage du formulaire de développement
|
|
showDevForm();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
// Formulaire de développement
|
|
const urlForm = document.getElementById("url-form");
|
|
if (urlForm) {
|
|
urlForm.addEventListener("submit", handleUrlFormSubmit);
|
|
}
|
|
|
|
// Gestion de la navigation arrière/avant du navigateur
|
|
window.addEventListener("popstate", handlePopState);
|
|
|
|
// Gestionnaires pour la prévisualisation
|
|
const closePreview = document.getElementById("close-preview");
|
|
if (closePreview) {
|
|
closePreview.addEventListener("click", hidePreview);
|
|
}
|
|
|
|
const downloadFile = document.getElementById("download-file");
|
|
if (downloadFile) {
|
|
downloadFile.addEventListener("click", downloadCurrentFile);
|
|
}
|
|
}
|
|
|
|
function handlePopState(event) {
|
|
console.log("Navigation arrière/avant détectée:", event.state);
|
|
|
|
if (event.state && event.state.baseUrl) {
|
|
// Restaurer la configuration
|
|
config.baseUrl = event.state.baseUrl;
|
|
config.currentPath = event.state.path || "";
|
|
|
|
updateConfigDisplay();
|
|
|
|
// Charger le contenu sans ajouter à l'historique (pour éviter les boucles)
|
|
loadBucketContents(config.currentPath, false);
|
|
}
|
|
}
|
|
|
|
function autoConfigureFromURL() {
|
|
const currentUrl = window.location.href;
|
|
const url = new URL(currentUrl);
|
|
|
|
// Configuration simplifiée : on prend juste le base URL sans le bucket
|
|
config.baseUrl = `${url.protocol}//${url.host}${url.pathname.replace("/index.html", "")}`;
|
|
|
|
// Vérifier s'il y a un fragment d'URL pour définir le chemin initial
|
|
if (url.hash) {
|
|
const hashPath = url.hash.replace("#/", "");
|
|
config.currentPath = hashPath;
|
|
}
|
|
|
|
updateConfigDisplay();
|
|
console.log("Configuration automatique:", config);
|
|
|
|
// Charger automatiquement le contenu si on a une config valide
|
|
if (config.baseUrl) {
|
|
loadBucketContents(config.currentPath, false);
|
|
}
|
|
}
|
|
|
|
function handleUrlFormSubmit(event) {
|
|
event.preventDefault();
|
|
|
|
const urlInput = document.getElementById("minio-url");
|
|
if (!urlInput) return;
|
|
|
|
const minioUrl = urlInput.value.trim();
|
|
|
|
if (!minioUrl) {
|
|
showError("Veuillez saisir une URL MinIO");
|
|
return;
|
|
}
|
|
|
|
parseMinioURL(minioUrl);
|
|
}
|
|
|
|
function parseMinioURL(url) {
|
|
try {
|
|
const urlObj = new URL(url);
|
|
|
|
// Configuration simplifiée : on prend l'URL complète comme base
|
|
config.baseUrl = url.replace(/\/$/, ""); // Supprimer le slash final si présent
|
|
config.currentPath = "";
|
|
|
|
updateConfigDisplay();
|
|
|
|
// Ajouter l'état initial à l'historique
|
|
const initialState = { path: "", baseUrl: config.baseUrl };
|
|
history.replaceState(initialState, "", window.location.href);
|
|
|
|
loadBucketContents();
|
|
} catch (error) {
|
|
showError("URL invalide: " + error.message);
|
|
console.error("Erreur de parsing URL:", error);
|
|
}
|
|
}
|
|
|
|
function updateConfigDisplay() {
|
|
const configInfo = document.getElementById("config-info");
|
|
if (configInfo) {
|
|
configInfo.innerHTML = `
|
|
<strong>Configuration actuelle:</strong><br>
|
|
URL de base: ${config.baseUrl}<br>
|
|
Chemin actuel: /${config.currentPath}
|
|
`;
|
|
}
|
|
}
|
|
|
|
function showDevForm() {
|
|
const devForm = document.getElementById("dev-form");
|
|
if (devForm) {
|
|
devForm.style.display = "block";
|
|
}
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorSection = document.getElementById("error-section");
|
|
const errorMessage = document.getElementById("error-message");
|
|
|
|
if (errorMessage) {
|
|
errorMessage.textContent = message;
|
|
}
|
|
|
|
if (errorSection) {
|
|
errorSection.style.display = "block";
|
|
|
|
// Masquer l'erreur après 5 secondes
|
|
setTimeout(() => {
|
|
errorSection.style.display = "none";
|
|
}, 5000);
|
|
}
|
|
}
|
|
|
|
function showLoading(show = true) {
|
|
const loading = document.getElementById("loading");
|
|
const table = document.getElementById("files-table");
|
|
|
|
if (loading) {
|
|
loading.style.display = show ? "block" : "none";
|
|
}
|
|
|
|
if (table) {
|
|
table.style.display = show ? "none" : "table";
|
|
}
|
|
}
|
|
|
|
function updateCurrentPathDisplay() {
|
|
const pathDisplay = document.getElementById("current-path");
|
|
if (pathDisplay) {
|
|
pathDisplay.textContent = "/" + config.currentPath;
|
|
}
|
|
}
|
|
|
|
async function loadBucketContents(path = "", pushToHistory = true) {
|
|
console.log(
|
|
"Debug loadBucketContents - path received:",
|
|
JSON.stringify(path),
|
|
);
|
|
|
|
if (!config.baseUrl) {
|
|
showError("Configuration URL manquante");
|
|
return;
|
|
}
|
|
|
|
showLoading(true);
|
|
config.currentPath = path;
|
|
updateCurrentPathDisplay();
|
|
|
|
// Ajouter à l'historique du navigateur si demandé
|
|
if (pushToHistory) {
|
|
const state = { path: path, baseUrl: config.baseUrl };
|
|
const url = path ? `#/${path}` : "#/";
|
|
history.pushState(state, "", url);
|
|
}
|
|
|
|
try {
|
|
// Construction de l'URL API avec delimiter
|
|
let apiUrl = `${config.baseUrl}?list-type=2&delimiter=/`;
|
|
|
|
// Construire le prefix pour l'API MinIO
|
|
if (path) {
|
|
// Pour un sous-dossier
|
|
const cleanPath = path.startsWith("/") ? path.substring(1) : path;
|
|
const fullPrefix = cleanPath + "/";
|
|
apiUrl += `&prefix=${encodeURIComponent(fullPrefix)}`;
|
|
}
|
|
|
|
console.log("Appel API MinIO:", apiUrl);
|
|
|
|
const response = await fetch(apiUrl);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
|
|
const xmlText = await response.text();
|
|
console.log("Réponse XML reçue:", xmlText);
|
|
|
|
const contents = parseMinioXML(xmlText);
|
|
displayContents(contents);
|
|
} catch (error) {
|
|
console.error("Erreur lors du chargement:", error);
|
|
showError("Erreur de chargement: " + error.message);
|
|
} finally {
|
|
showLoading(false);
|
|
}
|
|
}
|
|
|
|
function parseMinioXML(xmlText) {
|
|
const parser = new DOMParser();
|
|
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
|
|
|
|
const contents = {
|
|
folders: [],
|
|
files: [],
|
|
};
|
|
|
|
// Déterminer le préfixe attendu basé sur la navigation actuelle
|
|
let expectedPrefix = "";
|
|
if (config.currentPath) {
|
|
expectedPrefix = config.currentPath + "/";
|
|
}
|
|
|
|
// Parse CommonPrefixes (dossiers)
|
|
const prefixes = xmlDoc.getElementsByTagName("CommonPrefixes");
|
|
for (let prefix of prefixes) {
|
|
const prefixElement = prefix.getElementsByTagName("Prefix")[0];
|
|
if (prefixElement) {
|
|
const fullPrefix = prefixElement.textContent;
|
|
|
|
// Extraire le nom du dossier (dernière partie avant le /)
|
|
const folderName = fullPrefix.replace(/\/$/, "").split("/").pop();
|
|
|
|
if (folderName) {
|
|
// Construire le chemin pour la navigation
|
|
let folderPath;
|
|
if (config.currentPath) {
|
|
folderPath = config.currentPath + "/" + folderName;
|
|
} else {
|
|
folderPath = folderName;
|
|
}
|
|
|
|
console.log(
|
|
"Debug parsing - fullPrefix:",
|
|
fullPrefix,
|
|
"folderName:",
|
|
folderName,
|
|
"currentPath:",
|
|
config.currentPath,
|
|
"folderPath:",
|
|
folderPath,
|
|
);
|
|
|
|
contents.folders.push({
|
|
name: folderName,
|
|
path: folderPath,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse Contents (fichiers)
|
|
const objects = xmlDoc.getElementsByTagName("Contents");
|
|
for (let obj of objects) {
|
|
const keyElement = obj.getElementsByTagName("Key")[0];
|
|
const sizeElement = obj.getElementsByTagName("Size")[0];
|
|
const modifiedElement = obj.getElementsByTagName("LastModified")[0];
|
|
|
|
if (keyElement) {
|
|
const fullKey = keyElement.textContent;
|
|
const fileName = fullKey.split("/").pop();
|
|
|
|
// Vérifier que le fichier correspond au niveau de navigation actuel
|
|
let shouldInclude = false;
|
|
|
|
if (config.currentPath) {
|
|
// Dans un sous-dossier : vérifier que la clé commence par le bon préfixe
|
|
const currentPrefix = config.currentPath + "/";
|
|
if (fullKey.startsWith(currentPrefix)) {
|
|
// S'assurer qu'on n'affiche que les fichiers directs (pas dans des sous-sous-dossiers)
|
|
const relativePath = fullKey.replace(currentPrefix, "");
|
|
if (!relativePath.includes("/")) {
|
|
shouldInclude = true;
|
|
}
|
|
}
|
|
} else {
|
|
// À la racine : afficher tous les fichiers qui ne sont pas dans des dossiers
|
|
if (!fullKey.includes("/")) {
|
|
shouldInclude = true;
|
|
}
|
|
}
|
|
|
|
// Exclure les fichiers de l'application
|
|
if (
|
|
shouldInclude &&
|
|
fileName &&
|
|
!["index.html", "style.css", "script.js"].includes(fileName)
|
|
) {
|
|
contents.files.push({
|
|
name: fileName,
|
|
size: sizeElement ? parseInt(sizeElement.textContent) : 0,
|
|
modified: modifiedElement
|
|
? new Date(modifiedElement.textContent)
|
|
: new Date(),
|
|
fullKey: fullKey,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return contents;
|
|
}
|
|
|
|
function displayContents(contents) {
|
|
const tbody = document.getElementById("files-tbody");
|
|
if (!tbody) return;
|
|
|
|
tbody.innerHTML = "";
|
|
|
|
// Bouton parent directory si on n'est pas à la racine
|
|
if (config.currentPath) {
|
|
const parentPath = config.currentPath.split("/").slice(0, -1).join("/");
|
|
const parentRow = createParentRow(parentPath);
|
|
tbody.appendChild(parentRow);
|
|
}
|
|
|
|
// Afficher les dossiers
|
|
contents.folders.forEach((folder) => {
|
|
const row = createFolderRow(folder);
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Afficher les fichiers
|
|
contents.files.forEach((file) => {
|
|
const row = createFileRow(file);
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
// Afficher le tableau
|
|
const filesTable = document.getElementById("files-table");
|
|
if (filesTable) {
|
|
filesTable.style.display = "table";
|
|
}
|
|
|
|
// Chercher et ouvrir automatiquement index.rst s'il existe
|
|
autoOpenIndexRst(contents.files);
|
|
}
|
|
|
|
function createParentRow(parentPath) {
|
|
const row = document.createElement("tr");
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-parent"></span>
|
|
<a href="#" class="folder-link parent-dir" data-path="${parentPath}">..</a>
|
|
</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
`;
|
|
|
|
const link = row.querySelector(".folder-link");
|
|
link.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
loadBucketContents(parentPath, true);
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
function createFolderRow(folder) {
|
|
const row = document.createElement("tr");
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-folder"></span>
|
|
<a href="#" class="folder-link" data-path="${folder.path}">${folder.name}/</a>
|
|
</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
<td>-</td>
|
|
`;
|
|
|
|
const link = row.querySelector(".folder-link");
|
|
link.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
loadBucketContents(folder.path, true);
|
|
});
|
|
|
|
return row;
|
|
}
|
|
|
|
function createFileRow(file) {
|
|
const row = document.createElement("tr");
|
|
const fileUrl = `${config.baseUrl}/${file.fullKey}`;
|
|
|
|
row.innerHTML = `
|
|
<td>
|
|
<span class="icon-file"></span>
|
|
<a href="#" class="file-link" data-file-url="${fileUrl}" data-file-name="${file.name}">${file.name}</a>
|
|
</td>
|
|
<td class="file-size">${formatFileSize(file.size)}</td>
|
|
<td class="file-date">${formatDate(file.modified)}</td>
|
|
<td>
|
|
<button class="copy-btn" data-url="${fileUrl}">Copier lien</button>
|
|
</td>
|
|
`;
|
|
|
|
const fileLink = row.querySelector(".file-link");
|
|
fileLink.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
showPreview(fileUrl, file.name);
|
|
});
|
|
|
|
const copyBtn = row.querySelector(".copy-btn");
|
|
copyBtn.addEventListener("click", () => copyToClipboard(fileUrl));
|
|
|
|
return row;
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return "0 B";
|
|
|
|
const k = 1024;
|
|
const sizes = ["B", "KB", "MB", "GB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
}
|
|
|
|
function formatDate(date) {
|
|
return date.toLocaleString("fr-FR", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
async function copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
console.log("Lien copié:", text);
|
|
|
|
// Feedback visuel temporaire
|
|
const btn = event.target;
|
|
const originalText = btn.textContent;
|
|
btn.textContent = "Copié !";
|
|
setTimeout(() => {
|
|
btn.textContent = originalText;
|
|
}, 1000);
|
|
} catch (error) {
|
|
console.error("Erreur copie clipboard:", error);
|
|
|
|
// Fallback pour les navigateurs plus anciens
|
|
const textArea = document.createElement("textarea");
|
|
textArea.value = text;
|
|
document.body.appendChild(textArea);
|
|
textArea.select();
|
|
document.execCommand("copy");
|
|
document.body.removeChild(textArea);
|
|
}
|
|
}
|
|
|
|
// Variables globales pour la prévisualisation
|
|
let currentPreviewFile = {
|
|
url: "",
|
|
name: "",
|
|
};
|
|
|
|
function detectFileType(fileName) {
|
|
const extension = fileName.toLowerCase().split(".").pop();
|
|
|
|
const fileTypes = {
|
|
images: ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp"],
|
|
text: ["txt", "json", "xml", "csv", "log"],
|
|
code: [
|
|
"js",
|
|
"css",
|
|
"html",
|
|
"htm",
|
|
"py",
|
|
"java",
|
|
"cpp",
|
|
"c",
|
|
"php",
|
|
"rb",
|
|
"go",
|
|
"rs",
|
|
"ts",
|
|
"jsx",
|
|
"tsx",
|
|
"vue",
|
|
"svelte",
|
|
"tex",
|
|
"latex",
|
|
],
|
|
markdown: ["md", "markdown"],
|
|
rst: ["rst", "rest"],
|
|
pdf: ["pdf"],
|
|
video: ["mp4", "avi", "mov", "wmv", "flv", "webm", "mkv"],
|
|
audio: ["mp3", "wav", "ogg", "aac", "flac", "m4a"],
|
|
archive: ["zip", "rar", "7z", "tar", "gz", "bz2"],
|
|
};
|
|
|
|
for (const [type, extensions] of Object.entries(fileTypes)) {
|
|
if (extensions.includes(extension)) {
|
|
return type;
|
|
}
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
function showPreview(fileUrl, fileName) {
|
|
currentPreviewFile = { url: fileUrl, name: fileName };
|
|
|
|
const previewSection = document.getElementById("preview-section");
|
|
const previewTitle = document.getElementById("preview-title");
|
|
const previewContent = document.getElementById("preview-content");
|
|
|
|
if (!previewSection || !previewTitle || !previewContent) {
|
|
console.error("Éléments de prévisualisation non trouvés");
|
|
return;
|
|
}
|
|
|
|
previewTitle.textContent = `Prévisualisation: ${fileName}`;
|
|
previewContent.innerHTML =
|
|
'<div style="text-align: center; padding: 20px;">Chargement...</div>';
|
|
|
|
previewSection.style.display = "flex";
|
|
|
|
const fileType = detectFileType(fileName);
|
|
|
|
switch (fileType) {
|
|
case "images":
|
|
showImagePreview(fileUrl, previewContent);
|
|
break;
|
|
case "text":
|
|
showTextPreview(fileUrl, previewContent);
|
|
break;
|
|
case "code":
|
|
showCodePreview(fileUrl, fileName, previewContent);
|
|
break;
|
|
case "markdown":
|
|
showMarkdownPreview(fileUrl, previewContent);
|
|
break;
|
|
case "rst":
|
|
showRstPreview(fileUrl, previewContent);
|
|
break;
|
|
case "pdf":
|
|
showPdfPreview(fileUrl, previewContent);
|
|
break;
|
|
case "video":
|
|
showVideoPreview(fileUrl, previewContent);
|
|
break;
|
|
case "audio":
|
|
showAudioPreview(fileUrl, previewContent);
|
|
break;
|
|
default:
|
|
showUnsupportedPreview(fileName, previewContent);
|
|
}
|
|
}
|
|
|
|
function showImagePreview(fileUrl, container) {
|
|
const img = document.createElement("img");
|
|
img.src = fileUrl;
|
|
img.alt = "Prévisualisation image";
|
|
|
|
img.onload = () => {
|
|
container.innerHTML = "";
|
|
container.appendChild(img);
|
|
};
|
|
|
|
img.onerror = () => {
|
|
container.innerHTML =
|
|
'<div class="preview-error">Erreur: Impossible de charger l\'image</div>';
|
|
};
|
|
}
|
|
|
|
function showTextPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then((text) => {
|
|
const pre = document.createElement("pre");
|
|
pre.textContent = text;
|
|
container.innerHTML = "";
|
|
container.appendChild(pre);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Erreur chargement texte:", error);
|
|
container.innerHTML =
|
|
'<div class="preview-error">Erreur: Impossible de charger le fichier texte</div>';
|
|
});
|
|
}
|
|
|
|
function detectLanguageFromExtension(fileName) {
|
|
const extension = fileName.toLowerCase().split(".").pop();
|
|
|
|
const languageMap = {
|
|
js: "javascript",
|
|
jsx: "jsx",
|
|
ts: "typescript",
|
|
tsx: "tsx",
|
|
py: "python",
|
|
java: "java",
|
|
c: "c",
|
|
cpp: "cpp",
|
|
cxx: "cpp",
|
|
cc: "cpp",
|
|
php: "php",
|
|
rb: "ruby",
|
|
go: "go",
|
|
rs: "rust",
|
|
html: "html",
|
|
htm: "html",
|
|
css: "css",
|
|
scss: "scss",
|
|
sass: "sass",
|
|
less: "less",
|
|
json: "json",
|
|
xml: "xml",
|
|
vue: "vue",
|
|
svelte: "svelte",
|
|
sh: "bash",
|
|
bash: "bash",
|
|
zsh: "bash",
|
|
fish: "bash",
|
|
ps1: "powershell",
|
|
sql: "sql",
|
|
r: "r",
|
|
matlab: "matlab",
|
|
m: "matlab",
|
|
swift: "swift",
|
|
kt: "kotlin",
|
|
scala: "scala",
|
|
clj: "clojure",
|
|
hs: "haskell",
|
|
lua: "lua",
|
|
pl: "perl",
|
|
tex: "latex",
|
|
latex: "latex",
|
|
md: "markdown",
|
|
markdown: "markdown",
|
|
yml: "yaml",
|
|
yaml: "yaml",
|
|
toml: "toml",
|
|
ini: "ini",
|
|
conf: "bash",
|
|
config: "bash",
|
|
};
|
|
|
|
return languageMap[extension] || "text";
|
|
}
|
|
|
|
function showCodePreview(fileUrl, fileName, container) {
|
|
fetch(fileUrl)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then((text) => {
|
|
const language = detectLanguageFromExtension(fileName);
|
|
|
|
const pre = document.createElement("pre");
|
|
const code = document.createElement("code");
|
|
|
|
if (language !== "text") {
|
|
code.className = `language-${language}`;
|
|
}
|
|
|
|
code.textContent = text;
|
|
pre.appendChild(code);
|
|
|
|
container.innerHTML = "";
|
|
container.appendChild(pre);
|
|
|
|
// Appliquer la coloration syntaxique si Prism est disponible
|
|
if (window.Prism && language !== "text") {
|
|
try {
|
|
Prism.highlightElement(code);
|
|
} catch (error) {
|
|
console.warn("Erreur coloration syntaxique:", error);
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Erreur chargement code:", error);
|
|
container.innerHTML =
|
|
'<div class="preview-error">Erreur: Impossible de charger le fichier</div>';
|
|
});
|
|
}
|
|
|
|
function showPdfPreview(fileUrl, container) {
|
|
const iframe = document.createElement("iframe");
|
|
iframe.src = fileUrl;
|
|
iframe.style.width = "100%";
|
|
iframe.style.height = "100%";
|
|
|
|
container.innerHTML = "";
|
|
container.appendChild(iframe);
|
|
}
|
|
|
|
function showVideoPreview(fileUrl, container) {
|
|
const video = document.createElement("video");
|
|
video.src = fileUrl;
|
|
video.controls = true;
|
|
video.style.maxWidth = "100%";
|
|
|
|
container.innerHTML = "";
|
|
container.appendChild(video);
|
|
}
|
|
|
|
function showAudioPreview(fileUrl, container) {
|
|
const audio = document.createElement("audio");
|
|
audio.src = fileUrl;
|
|
audio.controls = true;
|
|
audio.style.width = "100%";
|
|
|
|
container.innerHTML = "";
|
|
container.appendChild(audio);
|
|
}
|
|
|
|
function showUnsupportedPreview(fileName, container) {
|
|
const extension = fileName.toLowerCase().split(".").pop();
|
|
container.innerHTML = `
|
|
<div class="preview-error">
|
|
<p>Prévisualisation non disponible pour ce type de fichier (.${extension})</p>
|
|
<p>Utilisez le bouton "Télécharger" ci-dessous pour obtenir le fichier.</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function hidePreview() {
|
|
const previewSection = document.getElementById("preview-section");
|
|
if (previewSection) {
|
|
previewSection.style.display = "none";
|
|
}
|
|
|
|
currentPreviewFile = { url: "", name: "" };
|
|
}
|
|
|
|
function downloadCurrentFile() {
|
|
if (currentPreviewFile.url) {
|
|
const link = document.createElement("a");
|
|
link.href = currentPreviewFile.url;
|
|
link.download = currentPreviewFile.name;
|
|
link.target = "_blank";
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
}
|
|
|
|
// Fonction pour ouvrir automatiquement index.rst s'il existe
|
|
function autoOpenIndexRst(files) {
|
|
// Chercher un fichier index.rst dans la liste
|
|
const indexRstFile = files.find(
|
|
(file) => file.name.toLowerCase() === "index.rst",
|
|
);
|
|
|
|
if (indexRstFile) {
|
|
// Construire l'URL complète du fichier
|
|
const fileUrl = `${config.baseUrl}/${indexRstFile.fullKey}`;
|
|
|
|
// Ouvrir automatiquement la prévisualisation
|
|
console.log("Auto-ouverture de index.rst trouvé:", indexRstFile.name);
|
|
showPreview(fileUrl, indexRstFile.name);
|
|
} else {
|
|
// Pas d'index.rst trouvé, fermer le preview s'il est ouvert
|
|
console.log("Aucun index.rst trouvé, fermeture du preview");
|
|
hidePreview();
|
|
}
|
|
}
|
|
|
|
// Fonction simple de parsing Markdown vers HTML
|
|
function parseMarkdown(text) {
|
|
let html = text;
|
|
|
|
// Headers
|
|
html = html.replace(/^### (.*$)/gim, "<h3>$1</h3>");
|
|
html = html.replace(/^## (.*$)/gim, "<h2>$1</h2>");
|
|
html = html.replace(/^# (.*$)/gim, "<h1>$1</h1>");
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
html = html.replace(/__(.*?)__/g, "<strong>$1</strong>");
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
html = html.replace(/_(.*?)_/g, "<em>$1</em>");
|
|
|
|
// Code inline
|
|
html = html.replace(/`(.*?)`/g, "<code>$1</code>");
|
|
|
|
// Code blocks
|
|
html = html.replace(/```([\s\S]*?)```/g, "<pre><code>$1</code></pre>");
|
|
|
|
// Links
|
|
html = html.replace(
|
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
'<a href="$2" target="_blank">$1</a>',
|
|
);
|
|
|
|
// Images
|
|
html = html.replace(
|
|
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
|
'<img alt="$1" src="$2" style="max-width: 100%; height: auto;">',
|
|
);
|
|
|
|
// Lists
|
|
html = html.replace(/^\* (.*$)/gim, "<li>$1</li>");
|
|
html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
|
|
html = html.replace(/^\d+\. (.*$)/gim, "<li>$1</li>");
|
|
|
|
// Wrap consecutive <li> items in <ul>
|
|
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
|
|
return "<ul>" + match + "</ul>";
|
|
});
|
|
|
|
// Line breaks
|
|
html = html.replace(/\n\n/g, "</p><p>");
|
|
html = "<p>" + html + "</p>";
|
|
|
|
// Clean up empty paragraphs
|
|
html = html.replace(/<p><\/p>/g, "");
|
|
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
|
|
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
|
|
html = html.replace(/<p>(<ul>)/g, "$1");
|
|
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
|
|
html = html.replace(/<p>(<pre>)/g, "$1");
|
|
html = html.replace(/(<\/pre>)<\/p>/g, "$1");
|
|
|
|
return html;
|
|
}
|
|
|
|
// Fonction simple de parsing reStructuredText vers HTML
|
|
function parseRst(text) {
|
|
let html = text;
|
|
|
|
// Créer un système de placeholders pour éviter les liens imbriqués
|
|
const linkPlaceholders = new Map();
|
|
let placeholderIndex = 0;
|
|
|
|
// Fonction pour créer un placeholder unique
|
|
const createLinkPlaceholder = (linkText, linkUrl) => {
|
|
const placeholder = `__LINK_PLACEHOLDER_${placeholderIndex++}__`;
|
|
linkPlaceholders.set(
|
|
placeholder,
|
|
`<a href="${linkUrl}" target="_blank">${linkText}</a>`,
|
|
);
|
|
return placeholder;
|
|
};
|
|
|
|
// Headers (RST style with underlines) - ordre important pour éviter les conflits
|
|
html = html.replace(/^(.*)\n#{3,}$/gim, "<h1>$1</h1>");
|
|
html = html.replace(/^(.*)\n={3,}$/gim, "<h2>$1</h2>");
|
|
html = html.replace(/^(.*)\n-{3,}$/gim, "<h3>$1</h3>");
|
|
html = html.replace(/^(.*)\n~{3,}$/gim, "<h4>$1</h4>");
|
|
html = html.replace(/^(.*)\n\^{3,}$/gim, "<h5>$1</h5>");
|
|
|
|
// Metadata fields
|
|
html = html.replace(/^:([^:]+):\s*(.*)$/gim, (match, key, value) => {
|
|
// Remplacer les liens par des placeholders dans les métadonnées
|
|
const processedValue = value.replace(
|
|
/`([^<]+) <([^>]+)>`_/g,
|
|
(match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
},
|
|
);
|
|
return `<div class="rst-metadata"><span class="metadata-key">${key}:</span> <span class="metadata-value">${processedValue}</span></div>`;
|
|
});
|
|
|
|
// Process directives with their options and content - regex corrigée pour s'arrêter aux lignes non indentées
|
|
html = html.replace(
|
|
/\.\. ([^:]+)::([\s\S]*?)(?=\n\S|\.\.|$)/g,
|
|
(match, directive, block) => {
|
|
// Gestion spéciale pour big_button
|
|
const isBigButton = directive === "big_button";
|
|
const className = isBigButton
|
|
? "rst-big-button"
|
|
: `rst-directive rst-directive-${directive}`;
|
|
|
|
let result = `<div class="${className}" data-directive="${directive}">`;
|
|
|
|
if (block && block.trim()) {
|
|
const lines = block.split("\n");
|
|
let options = "";
|
|
let content = "";
|
|
let inContentSection = false;
|
|
|
|
// Séparer d'abord les lignes d'options et de contenu
|
|
let optionLines = [];
|
|
let contentLines = [];
|
|
let isInOptions = true;
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) {
|
|
// Ligne vide - si on est en section contenu, l'ajouter
|
|
if (!isInOptions) {
|
|
contentLines.push(line);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Vérifier si c'est une option (:key: value)
|
|
const optionMatch = line.match(/^[ \t]*:([^:]+):\s*(.*)$/);
|
|
|
|
if (optionMatch && isInOptions) {
|
|
// C'est une option et on est toujours dans la section options
|
|
optionLines.push(line);
|
|
} else {
|
|
// C'est du contenu - marquer qu'on est sorti de la section options
|
|
isInOptions = false;
|
|
contentLines.push(line);
|
|
}
|
|
}
|
|
|
|
// Traiter les options
|
|
for (const line of optionLines) {
|
|
const optionMatch = line.match(/^[ \t]*:([^:]+):\s*(.*)$/);
|
|
if (optionMatch) {
|
|
const [, key, value] = optionMatch;
|
|
const processedValue = value.replace(
|
|
/`([^<]+) <([^>]+)>`_/g,
|
|
(match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
},
|
|
);
|
|
options += `<div class="directive-option" data-option="${key}" data-value="${value}"><span class="option-key">${key}:</span> <span class="option-value">${processedValue}</span></div>`;
|
|
}
|
|
}
|
|
|
|
// Traiter le contenu - trouver l'indentation minimale du contenu uniquement
|
|
if (contentLines.length > 0) {
|
|
// Trouver l'indentation minimale parmi les lignes de contenu non vides
|
|
let minContentIndent = null;
|
|
for (const line of contentLines) {
|
|
if (line.trim()) {
|
|
const indentMatch = line.match(/^([ \t]*)/);
|
|
if (indentMatch) {
|
|
const indent = indentMatch[1];
|
|
if (
|
|
minContentIndent === null ||
|
|
indent.length < minContentIndent.length
|
|
) {
|
|
minContentIndent = indent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Construire le contenu en préservant l'indentation relative
|
|
for (const line of contentLines) {
|
|
if (!line.trim()) {
|
|
content += "\n";
|
|
} else {
|
|
let contentLine = line;
|
|
if (minContentIndent && line.startsWith(minContentIndent)) {
|
|
contentLine = line.substring(minContentIndent.length);
|
|
} else {
|
|
// Fallback : enlever toute indentation de début
|
|
contentLine = line.replace(/^[ \t]+/, "");
|
|
}
|
|
content += contentLine + "\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
result += options;
|
|
|
|
if (content.trim()) {
|
|
const processedContent = content
|
|
.trim()
|
|
.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
result += `<div class="directive-content">${processedContent}</div>`;
|
|
}
|
|
}
|
|
|
|
result += "</div>";
|
|
return result;
|
|
},
|
|
);
|
|
|
|
// Bold
|
|
html = html.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
|
|
|
|
// Italic
|
|
html = html.replace(/\*(.*?)\*/g, "<em>$1</em>");
|
|
|
|
// Code inline
|
|
html = html.replace(/``(.*?)``/g, "<code>$1</code>");
|
|
|
|
// Code blocks (:: at end of line followed by indented block)
|
|
html = html.replace(
|
|
/::\s*\n\n((?: .*\n?)+)/g,
|
|
"<pre><code>$1</code></pre>",
|
|
);
|
|
|
|
// Links restants dans le texte principal
|
|
html = html.replace(/`([^<]+) <([^>]+)>`_/g, (match, text, url) => {
|
|
return createLinkPlaceholder(text, url);
|
|
});
|
|
|
|
// Lists
|
|
html = html.replace(/^\* (.*$)/gim, "<li>$1</li>");
|
|
html = html.replace(/^- (.*$)/gim, "<li>$1</li>");
|
|
html = html.replace(/^\d+\. (.*$)/gim, "<li>$1</li>");
|
|
|
|
// Wrap consecutive <li> items in <ul>
|
|
html = html.replace(/(<li>.*<\/li>)/gs, (match) => {
|
|
return "<ul>" + match + "</ul>";
|
|
});
|
|
|
|
// Line breaks
|
|
html = html.replace(/\n\n/g, "</p><p>");
|
|
html = "<p>" + html + "</p>";
|
|
|
|
// Clean up empty paragraphs and fix structure
|
|
html = html.replace(/<p><\/p>/g, "");
|
|
html = html.replace(/<p>(<h[1-6]>)/g, "$1");
|
|
html = html.replace(/(<\/h[1-6]>)<\/p>/g, "$1");
|
|
html = html.replace(/<p>(<div class="rst-)/g, '<div class="rst-');
|
|
html = html.replace(/(<\/div>)<\/p>/g, "$1");
|
|
html = html.replace(/<p>(<ul>)/g, "$1");
|
|
html = html.replace(/(<\/ul>)<\/p>/g, "$1");
|
|
html = html.replace(/<p>(<pre>)/g, "$1");
|
|
html = html.replace(/(<\/pre>)<\/p>/g, "$1");
|
|
|
|
// Restaurer tous les liens à partir des placeholders
|
|
for (const [placeholder, linkHtml] of linkPlaceholders) {
|
|
html = html.replace(new RegExp(placeholder, "g"), linkHtml);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function showMarkdownPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then((text) => {
|
|
const htmlContent = parseMarkdown(text);
|
|
const div = document.createElement("div");
|
|
div.className = "markdown-content";
|
|
div.innerHTML = htmlContent;
|
|
container.innerHTML = "";
|
|
container.appendChild(div);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Erreur chargement markdown:", error);
|
|
container.innerHTML =
|
|
'<div class="preview-error">Erreur: Impossible de charger le fichier Markdown</div>';
|
|
});
|
|
}
|
|
|
|
function showRstPreview(fileUrl, container) {
|
|
fetch(fileUrl)
|
|
.then((response) => {
|
|
if (!response.ok) {
|
|
throw new Error(`Erreur HTTP: ${response.status}`);
|
|
}
|
|
return response.text();
|
|
})
|
|
.then((text) => {
|
|
const htmlContent = parseRst(text);
|
|
const div = document.createElement("div");
|
|
div.className = "rst-content";
|
|
div.innerHTML = htmlContent;
|
|
container.innerHTML = "";
|
|
container.appendChild(div);
|
|
})
|
|
.catch((error) => {
|
|
console.error("Erreur chargement RST:", error);
|
|
container.innerHTML =
|
|
'<div class="preview-error">Erreur: Impossible de charger le fichier reStructuredText</div>';
|
|
});
|
|
}
|
|
|